Merge "Structured Exception details for DMI"
authorBruno Sakoto <bruno.sakoto@bell.ca>
Tue, 5 Apr 2022 12:30:00 +0000 (12:30 +0000)
committerGerrit Code Review <gerrit@onap.org>
Tue, 5 Apr 2022 12:30:00 +0000 (12:30 +0000)
40 files changed:
checkstyle/pom.xml
checkstyle/src/main/CopyrightCheck.py [new file with mode: 0644]
checkstyle/src/main/resources/copyright-template.txt [new file with mode: 0644]
checkstyle/src/main/resources/ignore-files-config.csv [new file with mode: 0644]
checkstyle/src/main/resources/project-committers-config.csv [new file with mode: 0644]
checkstyle/src/main/test_CopyrightCheck.py [new file with mode: 0644]
cps-ncmp-rest/docs/openapi/components.yaml
cps-ncmp-rest/docs/openapi/ncmp-inventory.yml
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy
cps-ncmp-rest/src/test/resources/dmi_registration_all_singing_and_dancing.json
cps-ncmp-rest/src/test/resources/dmi_registration_updates_only.json
cps-ncmp-rest/src/test/resources/dmi_registration_without_properties.json
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandle.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandlesList.java [deleted file]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy
cps-rest/docs/openapi/cpsAdmin.yml
cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java
cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java
cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java
cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java
cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy [new file with mode: 0644]
csit/tests/cps-model-sync/cps-model-sync.robot
docs/cps-path.rst
docs/release-notes.rst

index 4e042e9..bd34368 100644 (file)
@@ -2,6 +2,7 @@
 <!--
   ============LICENSE_START=======================================================
   Copyright (C) 2020 Pantheon.tech
+  Modifications Copyright (C) 2022 Nordix Foundation
   ================================================================================
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
     <artifactId>checkstyle</artifactId>
     <version>3.1.0-SNAPSHOT</version>
 
+    <profiles>
+        <profile>
+            <id>Windows</id>
+            <activation>
+                <os>
+                    <family>Windows</family>
+                </os>
+            </activation>
+            <properties>
+                <script.executor>python</script.executor>
+            </properties>
+        </profile>
+        <profile>
+            <id>unix</id>
+            <activation>
+                <os>
+                    <family>unix</family>
+                </os>
+            </activation>
+            <properties>
+                <script.executor>python3</script.executor>
+            </properties>
+        </profile>
+    </profiles>
+
     <properties>
         <nexusproxy>https://nexus.onap.org</nexusproxy>
         <releaseNexusPath>/content/repositories/releases/</releaseNexusPath>
                 </plugin>
             </plugins>
         </pluginManagement>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>exec-maven-plugin</artifactId>
+                <version>1.6.0</version>
+                <executions>
+                    <execution>
+                        <id>copyright-check</id>
+                        <phase>verify</phase>
+                        <goals>
+                            <goal>exec</goal>
+                        </goals>
+                        <configuration>
+                            <executable>${script.executor}</executable>
+                            <workingDirectory>../checkstyle/src/main/</workingDirectory>
+                            <arguments>
+                                <argument>CopyrightCheck.py</argument>
+                                <argument>resources/project-committers-config.csv</argument>
+                                <argument>resources/copyright-template.txt</argument>
+                                <argument>resources/ignore-files-config.csv</argument>
+                            </arguments>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
     </build>
 
     <distributionManagement>
diff --git a/checkstyle/src/main/CopyrightCheck.py b/checkstyle/src/main/CopyrightCheck.py
new file mode 100644 (file)
index 0000000..8f1dbff
--- /dev/null
@@ -0,0 +1,262 @@
+#  ============LICENSE_START=======================================================
+#  Copyright (C) 2022 Nordix Foundation
+#  ================================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+#  SPDX-License-Identifier: Apache-2.0
+#  ============LICENSE_END=========================================================
+
+import subprocess
+import csv
+import re
+import datetime
+
+#constants
+import sys
+
+COMMITTERS_CONFIG_FILE = ''
+TEMPLATE_COPYRIGHT_FILE = ''
+IGNORE_FILE = ''
+if len(sys.argv) == 4:
+    COMMITTERS_CONFIG_FILE = sys.argv[1]
+    TEMPLATE_COPYRIGHT_FILE = sys.argv[2]
+    IGNORE_FILE = sys.argv[3]
+
+BANNER = '=' * 120
+
+def main():
+    print(BANNER + '\nCopyright Check Python Script:')
+    PermissionsCheck()
+
+    committerEmailExtension = GetCommitterEmailExtension()
+    projectCommitters = ReadProjectCommittersConfigFile()
+
+    CheckCommitterInConfigFile(committerEmailExtension, projectCommitters)
+
+    alteredFiles = FindAlteredFiles()
+
+    if alteredFiles:
+        issueCounter = CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension)
+    else:
+        issueCounter = 0
+
+    print(str(issueCounter) + ' issue(s) found after '+ str(len(alteredFiles)) + ' altered file(s) checked')
+    print(BANNER)
+
+
+# Check that Script has access to command line functions to use git
+def PermissionsCheck():
+   if 'permission denied' in subprocess.run('git', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').lower():
+       print('Error, I may not have the necessary permissions. Exiting...')
+       print(BANNER)
+       sys.exit()
+   else:
+       return
+
+# Returns List of Strings of file tracked by git which have been changed/added
+def FindAlteredFiles():
+    ignoreFilePaths = GetIgnoredFiles()
+
+    #Before Stage lower case d removes deleted files
+    stream = subprocess.run('git diff --name-only --diff-filter=d', shell=True, stdout=subprocess.PIPE)
+    fileNames = stream.stdout.decode('utf-8')
+    #Staged
+    stream = subprocess.run('git diff --name-only --cached --diff-filter=d', shell=True, stdout=subprocess.PIPE)
+    fileNames += '\n' + stream.stdout.decode('utf-8')
+    #New committed
+    stream = subprocess.run('git diff --name-only HEAD^ HEAD --diff-filter=d', shell=True, stdout=subprocess.PIPE)
+    fileNames += '\n' + stream.stdout.decode('utf-8')
+
+    #String to list of strings
+    alteredFiles = fileNames.split("\n")
+
+    #Remove duplicates
+    alteredFiles = list(dict.fromkeys(alteredFiles))
+
+    #Remove blank string(s)
+    alteredFiles = list(filter(None, alteredFiles))
+
+    #Remove ignored-extensions
+    alteredFiles = list(filter(lambda fileName: not re.match("|".join(ignoreFilePaths), fileName), alteredFiles))
+
+    return alteredFiles
+
+# Get the email of the most recent committer
+def GetCommitterEmailExtension():
+    email = subprocess.run('git show -s --format=\'%ce\'', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip('\n')
+    return email[email.index('@'):]
+
+# Read the config file with names of companies and respective email extensions
+def ReadProjectCommittersConfigFile():
+    try:
+        with open(COMMITTERS_CONFIG_FILE, 'r') as file:
+            reader = csv.reader(file, delimiter=',')
+            projectCommitters = {row[0]:row[1] for row in reader}
+        projectCommitters.pop('email')  #Remove csv header
+    except FileNotFoundError:
+        print('Unable to open Project Committers Config File, have the command line arguments been set?')
+        print(BANNER)
+        sys.exit()
+    return projectCommitters
+
+def CheckCommitterInConfigFile(committerEmailExtension, projectCommitters):
+    if not committerEmailExtension in projectCommitters:
+        print('Error, Committer email is not included in config file.')
+        print('If your company is new to the project please make appropriate changes to project-committers-config.csv')
+        print('for Copyright Check to work.')
+        print('Exiting...')
+        print(BANNER)
+        sys.exit()
+    else:
+        return True
+
+# Read config file with list of files to ignore
+def GetIgnoredFiles():
+    try:
+        with open(IGNORE_FILE, 'r') as file:
+            reader = csv.reader(file)
+            ignoreFilePaths = [row[0] for row in reader]
+        ignoreFilePaths.pop(0)  #Remove csv header
+        ignoreFilePaths = [filePath.replace('*', '.*') for filePath in ignoreFilePaths]
+    except FileNotFoundError:
+        print('Unable to open File Ignore Config File, have the command line arguments been set?')
+        print(BANNER)
+        sys.exit()
+    return ignoreFilePaths
+
+# Read the template copyright file
+def GetCopyrightTemplate():
+    try:
+        with open(TEMPLATE_COPYRIGHT_FILE, 'r') as file:
+            copyrightTemplate = file.readlines()
+    except FileNotFoundError:
+        print('Unable to open Template Copyright File, have the command line arguments been set?')
+        print(BANNER)
+        sys.exit()
+    return copyrightTemplate
+
+def GetProjectRootDir():
+    return subprocess.run('git rev-parse --show-toplevel', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip('\n') + '/'
+
+# Get the Copyright from the altered file
+def ParseFileCopyright(fileObject):
+    global issueCounter
+    copyrightFlag = False
+    copyrightInFile = {}
+    lineNumber = 1
+    for line in fileObject:
+        if 'LICENSE_START' in line:
+            copyrightFlag = True
+        if copyrightFlag:
+            copyrightInFile[lineNumber] = line
+        if 'LICENSE_END' in line:
+            break
+        lineNumber += 1
+
+    if not copyrightFlag:
+        print(fileObject.name + ' | no copyright found')
+        return {}, {}
+
+    copyrightSignatures = {}
+    copyrightLineNumbers = list(copyrightInFile.keys())
+    #Capture signature lines after LICENSE_START line
+    for lineNumber in copyrightLineNumbers:
+        if '=' not in copyrightInFile[lineNumber]:
+            copyrightSignatures[lineNumber] = copyrightInFile[lineNumber]
+            copyrightInFile.pop(lineNumber)
+        elif 'LICENSE_START' not in copyrightInFile[lineNumber]:
+            break
+
+    return (copyrightInFile, copyrightSignatures)
+
+# Remove the Block comment syntax
+def RemoveCommentBlock(fileCopyright):
+    # Comment Characters can very depending on file # *..
+    endOfCommentsIndex = list(fileCopyright.values())[0].index('=')
+    for key in fileCopyright:
+        fileCopyright[key] = fileCopyright[key][endOfCommentsIndex:]
+        if fileCopyright[key] == '':
+            fileCopyright[key] = '\n'
+
+    return fileCopyright
+
+def CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension):
+    issueCounter = 0
+    templateCopyright = GetCopyrightTemplate() #Get Copyright Template
+    projectRootDir = GetProjectRootDir()
+
+    for fileName in alteredFiles: # Not removed files
+        try:
+            with open(projectRootDir + fileName, 'r') as fileObject:
+                (fileCopyright, fileSignatures) = ParseFileCopyright(fileObject)
+
+            #Empty dict evaluates to false
+            if fileCopyright and fileSignatures:
+                fileCopyright = RemoveCommentBlock(fileCopyright)
+                issueCounter += CheckCopyrightFormat(fileCopyright, templateCopyright, projectRootDir + fileName)
+                committerCompany = projectCommitters[committerEmailExtension]
+                issueCounter += CheckCopyrightSignature(fileSignatures, committerCompany, projectRootDir + fileName)
+            else:
+                issueCounter += 1
+
+        except FileNotFoundError:
+            issueCounter += 1
+            print('Unable to find file ' + projectRootDir + fileName)
+    return issueCounter
+
+# Check that the filecopyright matches the template copyright and print comparison
+def CheckCopyrightFormat(copyrightInFile, templateCopyright, filePath):
+    issueCounter = 0
+    errorWithComparison = ''
+    for copyrightInFileKey, templateLine in zip(copyrightInFile, templateCopyright):
+        if copyrightInFile[copyrightInFileKey] != templateLine:
+            issueCounter += 1
+            errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' read \t  ' + repr(copyrightInFile[copyrightInFileKey]) + '\n'
+            errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' expected ' + repr(templateLine) + '\n'
+    if errorWithComparison != '':
+        print(errorWithComparison.rstrip('\n'))
+    return issueCounter
+
+# Check the signatures and compare with committer signature and current year
+def CheckCopyrightSignature(copyrightSignatures, committerCompany, filePath):
+    issueCounter = 0
+    errorWithSignature = ''
+    signatureExists = False #signatureExistsForCommitter
+    afterFirstLine = False #afterFirstCopyright
+    for key in copyrightSignatures:
+        if afterFirstLine and 'Modifications Copyright' not in copyrightSignatures[key]:
+            issueCounter += 1
+            errorWithSignature += filePath + ' | line ' + str(key) + ' expected Modifications Copyright\n'
+        elif not afterFirstLine and 'Copyright' not in copyrightSignatures[key]:
+            issueCounter += 1
+            errorWithSignature += filePath + ' | line ' + str(key) + ' expected Copyright\n'
+        if committerCompany in copyrightSignatures[key]:
+            signatureExists = True
+            signatureYear = int(re.findall(r'\d+', copyrightSignatures[key])[-1])
+            currentYear = datetime.date.today().year
+            if signatureYear != currentYear:
+                issueCounter += 1
+                errorWithSignature += filePath + ' | line ' + str(key) + ' update year to include ' + str(currentYear) + '\n'
+        afterFirstLine = True
+
+    if not signatureExists:
+        issueCounter += 1
+        errorWithSignature += filePath + ' | missing company name and year for ' + committerCompany
+
+    if errorWithSignature != '':
+        print(errorWithSignature.rstrip('\n'))
+
+    return issueCounter
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
diff --git a/checkstyle/src/main/resources/copyright-template.txt b/checkstyle/src/main/resources/copyright-template.txt
new file mode 100644 (file)
index 0000000..205e0ca
--- /dev/null
@@ -0,0 +1,16 @@
+============LICENSE_START=======================================================
+================================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+SPDX-License-Identifier: Apache-2.0
+============LICENSE_END=========================================================
diff --git a/checkstyle/src/main/resources/ignore-files-config.csv b/checkstyle/src/main/resources/ignore-files-config.csv
new file mode 100644 (file)
index 0000000..4f7394f
--- /dev/null
@@ -0,0 +1,6 @@
+file path
+*checkstyle/*
+*.json
+*.yang
+*.rst
+*.csv
\ No newline at end of file
diff --git a/checkstyle/src/main/resources/project-committers-config.csv b/checkstyle/src/main/resources/project-committers-config.csv
new file mode 100644 (file)
index 0000000..85ee43b
--- /dev/null
@@ -0,0 +1,3 @@
+email,signature
+@est.tech,Nordix Foundation
+@bell.ca,Bell Canada
\ No newline at end of file
diff --git a/checkstyle/src/main/test_CopyrightCheck.py b/checkstyle/src/main/test_CopyrightCheck.py
new file mode 100644 (file)
index 0000000..177f9d4
--- /dev/null
@@ -0,0 +1,441 @@
+#  ============LICENSE_START=======================================================
+#  Copyright (C) 2022 Nordix Foundation
+#  ================================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+#  SPDX-License-Identifier: Apache-2.0
+#  ============LICENSE_END=========================================================
+
+import datetime
+import sys
+import unittest
+from unittest import mock
+from unittest.mock import MagicMock
+import io
+
+import CopyrightCheck
+
+BANNER = '=' * 120
+
+def MockStdout(command):
+    mock_stdout = MagicMock()
+    mock_stdout.configure_mock(**{"stdout.decode.return_value": command})
+    return mock_stdout
+
+class TestCopyrightCheck(unittest.TestCase):
+
+    @mock.patch('subprocess.run')
+    def test_PermissionsCheckFalse(self, mock_subprocess_run):
+        mock_subprocess_run.return_value = MockStdout('Permission denied')
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with self.assertRaises(SystemExit):
+            CopyrightCheck.PermissionsCheck()
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(),
+                         'Error, I may not have the necessary permissions. Exiting...\n' + BANNER + '\n')
+
+    @mock.patch('subprocess.run')
+    def test_PermissionsCheckTrue(self, mock_subprocess_run):
+        mock_subprocess_run.return_value = MockStdout(
+            'usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]...')
+        CopyrightCheck.PermissionsCheck()   # Assert no error thrown
+
+    @mock.patch('CopyrightCheck.GetIgnoredFiles')
+    @mock.patch('subprocess.run')
+    def test_FindAlteredFiles(self, mock_subprocess_run, mock_GetIgnoredFiles):
+        mock_GetIgnoredFiles.return_value = ['.*.json', 'dir/.*']
+        mock_subprocess_run.return_value = MockStdout('File1.json\nFile2.java\nFile2.java\ndir/File3.java')
+        result = CopyrightCheck.FindAlteredFiles()
+        # Duplicates, .json and files in 'dir' removed
+        self.assertEqual(result, ['File2.java'])
+
+    @mock.patch('CopyrightCheck.GetIgnoredFiles')
+    @mock.patch('subprocess.run')
+    def test_FindAlteredFilesWithNoFileChanges(self, mock_subprocess_run, mock_GetIgnoredFiles):
+        mock_GetIgnoredFiles.return_value = ['.*.json', 'dir/.*']
+        mock_subprocess_run.return_value = MockStdout('File1.json\ndir/File3.java')
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.FindAlteredFiles()
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(result, [])
+        self.assertEqual(capturedOutput.getvalue(), '')
+
+    @mock.patch('subprocess.run')
+    def test_GetCommitterEmailExtension(self, mock_subprocess_run):
+        mock_subprocess_run.return_value = MockStdout('a.committer.name@address.com')
+        result = CopyrightCheck.GetCommitterEmailExtension()
+        self.assertEqual(result, '@address.com')
+
+    def test_ReadProjectCommittersConfigFile(self):
+        mock_open = mock.mock_open(read_data="email,signature\n@address.com,Company Name")
+        with mock.patch('builtins.open', mock_open):
+            result = CopyrightCheck.ReadProjectCommittersConfigFile()
+        self.assertEqual(result, {'@address.com': 'Company Name'})
+
+    @mock.patch('CopyrightCheck.open')
+    def test_ReadProjectCommittersConfigFileError(self, mock_OpenFile):
+        mock_OpenFile.side_effect = FileNotFoundError
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with self.assertRaises(SystemExit):
+            CopyrightCheck.ReadProjectCommittersConfigFile()
+        sys.stdout = sys.__stdout__
+        expectedOutput = ('Unable to open Project Committers Config File, have the command line arguments been set?\n' +
+                          BANNER + '\n')
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+    def test_CheckCommitterInConfigFileTrue(self):
+        committerEmailExtension = '@address.com'
+        projectCommitters = {'@address.com': 'Company Name'}
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCommitterInConfigFile(committerEmailExtension, projectCommitters)
+        sys.stdout = sys.__stdout__
+        self.assertTrue(result)
+        self.assertEqual(capturedOutput.getvalue(), "")
+
+    def test_CheckCommitterInConfigFileFalse(self):
+        committerEmailExtension = '@address.com'
+        projectCommitters = {'@anotheraddress.com': 'Another Company Name'}
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with self.assertRaises(SystemExit):
+            CopyrightCheck.CheckCommitterInConfigFile(committerEmailExtension, projectCommitters)
+        sys.stdout = sys.__stdout__
+        expectedOutput = ('Error, Committer email is not included in config file.\n' +
+                          'If your company is new to the project please make appropriate changes to project-committers-config.csv\n' +
+                          'for Copyright Check to work.\n' +
+                          'Exiting...\n' + BANNER + '\n')
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+    def test_GetIgnoredFiles(self):
+        mock_open = mock.mock_open(read_data="file path\n*checkstyle/*\n*.json")
+        with mock.patch('builtins.open', mock_open):
+            result = CopyrightCheck.GetIgnoredFiles()
+        self.assertEqual(result, [".*checkstyle/.*", ".*.json"])
+
+    @mock.patch('CopyrightCheck.open')
+    def test_GetIgnoredFilesError(self, mock_OpenFile):
+        mock_OpenFile.side_effect = FileNotFoundError
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with self.assertRaises(SystemExit):
+            CopyrightCheck.GetIgnoredFiles()
+        sys.stdout = sys.__stdout__
+        expectedOutput = ('Unable to open File Ignore Config File, have the command line arguments been set?\n' +
+                          BANNER + '\n')
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+    def test_GetCopyrightTemplate(self):
+        mock_open = mock.mock_open(read_data="****\nThis is a\nCopyright File\n****")
+        with mock.patch('builtins.open', mock_open):
+            result = CopyrightCheck.GetCopyrightTemplate()
+        self.assertEqual(result, ["****\n", "This is a\n", "Copyright File\n", "****"])
+
+    @mock.patch('CopyrightCheck.open')
+    def test_GetCopyrightTemplateError(self, mock_OpenFile):
+        mock_OpenFile.side_effect = FileNotFoundError
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with self.assertRaises(SystemExit):
+            CopyrightCheck.GetCopyrightTemplate()
+        sys.stdout = sys.__stdout__
+        expectedOutput = ('Unable to open Template Copyright File, have the command line arguments been set?\n' +
+                          BANNER + '\n')
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+    @mock.patch('subprocess.run')
+    def test_GetProjectRootDir(self, mock_subprocess_run):
+        mock_subprocess_run.return_value = MockStdout('project/root/dir\n')
+        result = CopyrightCheck.GetProjectRootDir()
+        self.assertEqual(result, 'project/root/dir/')
+
+
+    def test_ParseFileCopyright(self):
+        readFromFile = ["#Before lines will not be included\n",
+                        "#===LICENSE_START===\n",
+                        "#Copyright (C) 0000 Some Company\n",
+                        "#A line without signature\n",
+                        "#===============================\n",
+                        "#This is the start of the Copyright\n",
+                        "#===LICENSE_END===\n",
+                        "After lines will not be included"]
+        copyright, signatures = CopyrightCheck.ParseFileCopyright(readFromFile)
+        self.assertEqual(copyright, {2: "#===LICENSE_START===\n",
+                                     5: "#===============================\n",
+                                     6: "#This is the start of the Copyright\n",
+                                     7: "#===LICENSE_END===\n"})
+        self.assertEqual(signatures, {3: "#Copyright (C) 0000 Some Company\n",
+                                      4: "#A line without signature\n"})
+
+    def test_ParseFileCopyrightNoCopyright(self):
+        fileObject = io.StringIO("#This is not\na copyright\n")
+        fileObject.name = 'some/file/name'
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        copyright, signatures = CopyrightCheck.ParseFileCopyright(fileObject)
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(copyright, {})
+        self.assertEqual(signatures, {})
+        self.assertEqual(capturedOutput.getvalue(), 'some/file/name | no copyright found\n')
+
+    def test_RemoveCommentBlock(self):
+        commentCharactersList = ['# ', '* ', '#  ', '*  ']
+
+        for commentCharacters in commentCharactersList:
+            copyright = {1: commentCharacters + '===LICENSE_START===\n',
+                         2: '\n',
+                         3: commentCharacters + 'This is the License\n',
+                         4: commentCharacters + '===LICENSE_END===\n'}
+            result = CopyrightCheck.RemoveCommentBlock(copyright)
+            self.assertEqual(result, {1: '===LICENSE_START===\n',
+                                      2: '\n',
+                                      3: 'This is the License\n',
+                                      4: '===LICENSE_END===\n'})
+
+    @mock.patch('CopyrightCheck.open')
+    @mock.patch('CopyrightCheck.GetProjectRootDir')
+    @mock.patch('CopyrightCheck.GetCopyrightTemplate')
+    def test_CheckCopyrightForFileNotFound(self, mock_GetCopyrightTemplate, mock_GetProjectRootDir, mock_OpenFile):
+        mock_GetCopyrightTemplate.return_value = 'some-copyright-template'
+        mock_GetProjectRootDir.return_value = 'some/project/root/dir/'
+        mock_OpenFile.side_effect = FileNotFoundError
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightForFiles(['some-file.java'], {}, [])
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(), 'Unable to find file some/project/root/dir/some-file.java\n')
+        self.assertEqual(result, 1)
+
+    @mock.patch('CopyrightCheck.ParseFileCopyright')
+    @mock.patch('CopyrightCheck.GetProjectRootDir')
+    @mock.patch('CopyrightCheck.GetCopyrightTemplate')
+    def test_CheckCopyrightForFileWithNoCopyright(self, mock_GetCopyrightTemplate, mock_GetProjectRootDir,
+                                                  mock_ParseFileCopyright):
+        mock_GetCopyrightTemplate.return_value = 'some-copyright-template'
+        mock_GetProjectRootDir.return_value = 'some/project/root/dir/'
+        mock_ParseFileCopyright.return_value = ({}, {})
+        mock_open = mock.mock_open(read_data="some-file-content")
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with mock.patch('builtins.open', mock_open):
+            result = CopyrightCheck.CheckCopyrightForFiles(['some-file.java'], {}, [])
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(), "")
+        self.assertEqual(result, 1)
+
+
+    @mock.patch('CopyrightCheck.CheckCopyrightSignature')
+    @mock.patch('CopyrightCheck.CheckCopyrightFormat')
+    @mock.patch('CopyrightCheck.ParseFileCopyright')
+    @mock.patch('CopyrightCheck.GetProjectRootDir')
+    @mock.patch('CopyrightCheck.GetCopyrightTemplate')
+    def test_CheckCopyrightForFilesWhichAreRight(self, mock_GetCopyrightTemplate, mock_GetProjectRootDir,
+                                                  mock_ParseFileCopyright, mock_CheckCopyrightFormat,
+                                                  mock_CheckCopyrightSignature):
+        mock_GetCopyrightTemplate.return_value = 'some-copyright-template'
+        mock_GetProjectRootDir.return_value = 'some/project/root/dir/'
+        mock_ParseFileCopyright.return_value = ({1: '# =some-copyright-line'}, {2: '# =some-signature-line'})
+        mock_open = mock.mock_open(read_data="# =some-file-content")
+        mock_CheckCopyrightFormat.return_value = 0
+        mock_CheckCopyrightSignature.return_value = 0
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with mock.patch('builtins.open', mock_open):
+            result = CopyrightCheck.CheckCopyrightForFiles(['some-file.java', 'another-file.java'], {'@address.com': 'Some Company'}, '@address.com')
+        sys.stdout = sys.__stdout__
+        self.assertEqual(result, 0)
+        self.assertEqual(capturedOutput.getvalue(), "")
+
+        mock_GetCopyrightTemplate.assert_called_once_with()
+        mock_GetProjectRootDir.assert_called_once_with()
+        self.assertEqual(mock_ParseFileCopyright.call_count, 2)
+        mock_CheckCopyrightFormat.assert_has_calls([
+            mock.call({1: '=some-copyright-line'}, 'some-copyright-template', 'some/project/root/dir/some-file.java'),
+            mock.call({1: '=some-copyright-line'}, 'some-copyright-template', 'some/project/root/dir/another-file.java')
+        ])
+        mock_CheckCopyrightSignature.assert_has_calls([
+            mock.call({2: '# =some-signature-line'}, 'Some Company', 'some/project/root/dir/some-file.java'),
+            mock.call({2: '# =some-signature-line'}, 'Some Company', 'some/project/root/dir/another-file.java')
+        ])
+        self.assertEqual(mock_CheckCopyrightFormat.call_count, 2)
+        self.assertEqual(mock_CheckCopyrightSignature.call_count, 2)
+
+
+    def test_CheckCopyrightFormatWhichIsWrong(self):
+        fileCopyright = {1: '---LICENSE_START---\n',
+                         2: 'This is the license typo\n',
+                         3: '',
+                         4: '===license_end===\n'}
+        templateCopyright = ['===LICENSE_START===\n',
+                             'This is the license\n',
+                             '\n',
+                             '===LICENSE_END===\n']
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightFormat(fileCopyright, templateCopyright, 'some/file/path')
+        sys.stdout = sys.__stdout__
+
+        expectedOutput = ("some/file/path | line  1 read \t  '---LICENSE_START---\\n'\n" +
+                          "some/file/path | line  1 expected '===LICENSE_START===\\n'\n" +
+                          "some/file/path | line  2 read \t  'This is the license typo\\n'\n" +
+                          "some/file/path | line  2 expected 'This is the license\\n'\n" +
+                          "some/file/path | line  3 read \t  ''\n" +
+                          "some/file/path | line  3 expected '\\n'\n" +
+                          "some/file/path | line  4 read \t  '===license_end===\\n'\n" +
+                          "some/file/path | line  4 expected '===LICENSE_END===\\n'\n")
+
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+        self.assertEqual(result, 4)
+
+    def test_CheckCopyrightFormatWhichIsCorrect(self):
+        fileCopyright = {1: '===LICENSE_START===\n',
+                         2: 'This is the license\n',
+                         3: '\n',
+                         4: '===LICENSE_END===\n'}
+        templateCopyright = ['===LICENSE_START===\n',
+                             'This is the license\n',
+                             '\n',
+                             '===LICENSE_END===\n']
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightFormat(fileCopyright, templateCopyright, 'some/file/path')
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(), "")
+        self.assertEqual(result, 0)
+
+    def test_CheckCopyrightSignatureWhichIsWrong(self):
+        fileSignatures = {1: "Trigger expected Copy-right",
+                          2: "Trigger expected Mod Copy-right"}
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightSignature(fileSignatures, 'Some-Company', 'some/file/path')
+        sys.stdout = sys.__stdout__
+
+        expectedOutput = ("some/file/path | line 1 expected Copyright\n" +
+                          "some/file/path | line 2 expected Modifications Copyright\n" +
+                          "some/file/path | missing company name and year for Some-Company\n")
+
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+        self.assertEqual(result, 3)
+
+    def test_CheckCopyrightSignatureWhichHasWrongYear(self):
+        currentYear = datetime.date.today().year
+        fileSignatures = {1: "Copyright (C) 1999 Some-Company"}
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightSignature(fileSignatures, 'Some-Company', 'some/file/path')
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(),
+                         "some/file/path | line 1 update year to include " + str(currentYear) + "\n")
+        self.assertEqual(result, 1)
+
+    def test_CheckCopyrightSignatureWhichIsRight(self):
+        currentYear = datetime.date.today().year
+        fileSignatures = {1: "Copyright (C) " + str(currentYear) + " Some-Company"}
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightSignature(fileSignatures, 'Some-Company', 'some/file/path')
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(), "")
+        self.assertEqual(result, 0)
+
+    @mock.patch('CopyrightCheck.CheckCopyrightForFiles')
+    @mock.patch('CopyrightCheck.FindAlteredFiles')
+    @mock.patch('CopyrightCheck.CheckCommitterInConfigFile')
+    @mock.patch('CopyrightCheck.ReadProjectCommittersConfigFile')
+    @mock.patch('CopyrightCheck.GetCommitterEmailExtension')
+    @mock.patch('CopyrightCheck.PermissionsCheck')
+    def test_Main(self, mock_PermissionsCheck, mock_GetCommitterEmailExtension, mock_ReadProjectCommittersConfigFile,
+                  mock_CheckCommitterInConfigFile, mock_FindAlteredFiles, mock_CheckCopyrightForFiles):
+
+        mock_GetCommitterEmailExtension.return_value = '@address.com'
+        mock_ReadProjectCommittersConfigFile.return_value = {'@address.com', 'Some Company'}
+        mock_FindAlteredFiles.return_value = ['some-file.java']
+        mock_CheckCopyrightForFiles.return_value = 5
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+
+        CopyrightCheck.main()
+
+        sys.stdout = sys.__stdout__
+
+        expectedOutput = (BANNER + '\nCopyright Check Python Script:\n' +
+                          '5 issue(s) found after 1 altered file(s) checked\n' +
+                          BANNER + '\n')
+
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+        mock_PermissionsCheck.assert_called_once_with()
+        mock_GetCommitterEmailExtension.assert_called_once_with()
+        mock_ReadProjectCommittersConfigFile.assert_called_once_with()
+        mock_CheckCommitterInConfigFile.assert_called_once_with('@address.com', {'@address.com', 'Some Company'})
+        mock_FindAlteredFiles.assert_called_once_with()
+        mock_CheckCopyrightForFiles.assert_called_once_with(['some-file.java'], {'@address.com', 'Some Company'}, '@address.com')
+
+    @mock.patch('CopyrightCheck.CheckCopyrightForFiles')
+    @mock.patch('CopyrightCheck.FindAlteredFiles')
+    @mock.patch('CopyrightCheck.CheckCommitterInConfigFile')
+    @mock.patch('CopyrightCheck.ReadProjectCommittersConfigFile')
+    @mock.patch('CopyrightCheck.GetCommitterEmailExtension')
+    @mock.patch('CopyrightCheck.PermissionsCheck')
+    def test_MainNoFiles(self, mock_PermissionsCheck, mock_GetCommitterEmailExtension, mock_ReadProjectCommittersConfigFile,
+                  mock_CheckCommitterInConfigFile, mock_FindAlteredFiles, mock_CheckCopyrightForFiles):
+
+        mock_GetCommitterEmailExtension.return_value = '@address.com'
+        mock_ReadProjectCommittersConfigFile.return_value = {'@address.com', 'Some Company'}
+        mock_FindAlteredFiles.return_value = []
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+
+        CopyrightCheck.main()
+
+        sys.stdout = sys.__stdout__
+
+        expectedOutput = (BANNER + '\nCopyright Check Python Script:\n' +
+                          '0 issue(s) found after 0 altered file(s) checked\n' +
+                          BANNER + '\n')
+
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+        mock_PermissionsCheck.assert_called_once_with()
+        mock_GetCommitterEmailExtension.assert_called_once_with()
+        mock_ReadProjectCommittersConfigFile.assert_called_once_with()
+        mock_CheckCommitterInConfigFile.assert_called_once_with('@address.com', {'@address.com', 'Some Company'})
+        mock_FindAlteredFiles.assert_called_once_with()
+        mock_CheckCopyrightForFiles.assert_not_called()
+
+
+if __name__ == '__main__':
+    unittest.main()
index 092c0a2..7719193 100644 (file)
@@ -86,6 +86,54 @@ components:
           items:
             type: string
           example: [my-cm-handle1, my-cm-handle2, my-cm-handle3]
+    DmiPluginRegistrationErrorResponse:
+      type: object
+      properties:
+        failedCreatedCmHandles:
+          type: array
+          items:
+            $ref: '#/components/schemas/CmHandlerRegistrationErrorResponse'
+          example: [
+            {
+              "cmHandle": "my-cm-handle-01",
+              "errorCode": "01",
+              "errorText": "cm-handle already exists"
+            }
+          ]
+        failedUpdatedCmHandles:
+          type: array
+          items:
+            $ref: '#/components/schemas/CmHandlerRegistrationErrorResponse'
+          example: [
+            {
+              "cmHandle": "my-cm-handle-02",
+              "errorCode": "02",
+              "errorText": "cm-handle does not exist"
+            }
+          ]
+        failedRemovedCmHandles:
+          type: array
+          items:
+            $ref: '#/components/schemas/CmHandlerRegistrationErrorResponse'
+          example: [
+            {
+              "cmHandle": "my-cm-handle-02",
+              "errorCode": "02",
+              "errorText": "cm-handle does not exist"
+            }
+          ]
+    CmHandlerRegistrationErrorResponse:
+      type: object
+      properties:
+        cmHandle:
+          type: string
+          example: my-cm-handle
+        errorCode:
+          type: string
+          example: '01'
+        errorText:
+          type: string
+          example: 'cm-handle already exists'
 
     RestInputCmHandle:
       required:
index 3cd8e8b..5e61d09 100755 (executable)
@@ -31,7 +31,7 @@ updateDmiRegistration:
           schema:
             $ref: 'components.yaml#/components/schemas/RestDmiPluginRegistration'
     responses:
-      204:
+      200:
         $ref: 'components.yaml#/components/responses/NoContent'
       400:
         $ref: 'components.yaml#/components/responses/BadRequest'
@@ -40,4 +40,7 @@ updateDmiRegistration:
       403:
         $ref: 'components.yaml#/components/responses/Forbidden'
       500:
-        $ref: 'components.yaml#/components/responses/InternalServerError'
+        content:
+          application/json:
+            schema:
+              $ref: 'components.yaml#/components/schemas/DmiPluginRegistrationErrorResponse'
index c9d26f2..e9a69b3 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021 Bell Canada
+ *  Copyright (C) 2021-2022 Bell Canada
  *  Modifications Copyright (C) 2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
 
 package org.onap.cps.ncmp.rest.controller;
 
+import java.util.List;
+import java.util.stream.Collectors;
 import javax.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status;
+import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse;
 import org.onap.cps.ncmp.rest.api.NetworkCmProxyInventoryApi;
+import org.onap.cps.ncmp.rest.model.CmHandlerRegistrationErrorResponse;
+import org.onap.cps.ncmp.rest.model.DmiPluginRegistrationErrorResponse;
 import org.onap.cps.ncmp.rest.model.RestDmiPluginRegistration;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -41,14 +48,57 @@ public class NetworkCmProxyInventoryController implements NetworkCmProxyInventor
 
     /**
      * Update DMI Plugin Registration (used for first registration also).
+     *
      * @param restDmiPluginRegistration the registration data
      */
     @Override
-    public ResponseEntity<Void> updateDmiPluginRegistration(
+    public ResponseEntity updateDmiPluginRegistration(
         final @Valid RestDmiPluginRegistration restDmiPluginRegistration) {
-        networkCmProxyDataService.updateDmiRegistrationAndSyncModule(
-            ncmpRestInputMapper.toDmiPluginRegistration(restDmiPluginRegistration));
-        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+        final DmiPluginRegistrationResponse dmiPluginRegistrationResponse =
+            networkCmProxyDataService.updateDmiRegistrationAndSyncModule(
+                ncmpRestInputMapper.toDmiPluginRegistration(restDmiPluginRegistration));
+        final DmiPluginRegistrationErrorResponse failedRegistrationErrorResponse =
+            getFailureRegistrationResponse(dmiPluginRegistrationResponse);
+        return allRegistrationsSuccessful(failedRegistrationErrorResponse)
+            ? new ResponseEntity<>(HttpStatus.OK)
+            : new ResponseEntity<>(failedRegistrationErrorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
+    }
+
+    private boolean allRegistrationsSuccessful(
+        final DmiPluginRegistrationErrorResponse dmiPluginRegistrationErrorResponse) {
+        return dmiPluginRegistrationErrorResponse.getFailedCreatedCmHandles().isEmpty()
+            && dmiPluginRegistrationErrorResponse.getFailedUpdatedCmHandles().isEmpty()
+            && dmiPluginRegistrationErrorResponse.getFailedRemovedCmHandles().isEmpty();
+
+    }
+
+    private DmiPluginRegistrationErrorResponse getFailureRegistrationResponse(
+        final DmiPluginRegistrationResponse dmiPluginRegistrationResponse) {
+        final var dmiPluginRegistrationErrorResponse = new DmiPluginRegistrationErrorResponse();
+        dmiPluginRegistrationErrorResponse.setFailedCreatedCmHandles(
+            getFailedResponses(dmiPluginRegistrationResponse.getCreatedCmHandles()));
+        dmiPluginRegistrationErrorResponse.setFailedUpdatedCmHandles(
+            getFailedResponses(dmiPluginRegistrationResponse.getUpdatedCmHandles()));
+        dmiPluginRegistrationErrorResponse.setFailedRemovedCmHandles(
+            getFailedResponses(dmiPluginRegistrationResponse.getRemovedCmHandles()));
+
+        return dmiPluginRegistrationErrorResponse;
+    }
+
+    private List<CmHandlerRegistrationErrorResponse> getFailedResponses(
+        final List<CmHandleRegistrationResponse> cmHandleRegistrationResponseList) {
+        return cmHandleRegistrationResponseList.stream()
+            .filter(cmHandleRegistrationResponse -> cmHandleRegistrationResponse.getStatus() == Status.FAILURE)
+            .map(this::toCmHandleRegistrationErrorResponse)
+            .collect(Collectors.toList());
+    }
+
+    private CmHandlerRegistrationErrorResponse toCmHandleRegistrationErrorResponse(
+        final CmHandleRegistrationResponse registrationResponse) {
+        return new CmHandlerRegistrationErrorResponse()
+            .cmHandle(registrationResponse.getCmHandle())
+            .errorCode(registrationResponse.getRegistrationError().errorCode)
+            .errorText(registrationResponse.getErrorText());
     }
 
 }
index 9b1c2e8..30b6beb 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021 Bell Canada
+ *  Copyright (C) 2021-2022 Bell Canada
  *  Modifications Copyright (C) 2021-2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,7 +24,11 @@ package org.onap.cps.ncmp.rest.controller
 import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.TestUtils
 import org.onap.cps.ncmp.api.NetworkCmProxyDataService
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse
 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
+import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse
+import org.onap.cps.ncmp.rest.model.CmHandlerRegistrationErrorResponse
+import org.onap.cps.ncmp.rest.model.DmiPluginRegistrationErrorResponse
 import org.onap.cps.ncmp.rest.model.RestDmiPluginRegistration
 import org.onap.cps.utils.JsonObjectMapper
 import org.spockframework.spring.SpringBean
@@ -36,6 +40,9 @@ import org.springframework.http.HttpStatus
 import org.springframework.http.MediaType
 import org.springframework.test.web.servlet.MockMvc
 import spock.lang.Specification
+
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_ALREADY_EXIST
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_DOES_NOT_EXIST
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
 
 @WebMvcTest(NetworkCmProxyInventoryController)
@@ -58,7 +65,7 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
     @Value('${rest.api.ncmp-inventory-base-path}/v1')
     def ncmpBasePathV1
 
-    def 'Dmi plugin registration #scenario' () {
+    def 'Dmi plugin registration #scenario'() {
         given: 'a dmi plugin registration with #scenario'
             def jsonData = TestUtils.getResourceFileContent(dmiRegistrationJson)
         and: 'the expected rest input as an object'
@@ -72,9 +79,9 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
                     .content(jsonData)
             ).andReturn().response
         then: 'the converted object is forwarded to the registration service'
-            1 * mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(mockDmiPluginRegistration)
+            1 * mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(mockDmiPluginRegistration) >> new DmiPluginRegistrationResponse()
         and: 'response status is no content'
-            response.status ==  HttpStatus.NO_CONTENT.value()
+            response.status == HttpStatus.OK.value()
         where: 'the following registration json is used'
             scenario                                                                       | dmiRegistrationJson
             'multiple services, added, updated and removed cm handles and many properties' | 'dmi_registration_all_singing_and_dancing.json'
@@ -82,7 +89,7 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
             'without any properties'                                                       | 'dmi_registration_without_properties.json'
     }
 
-    def 'Dmi plugin registration with invalid json' () {
+    def 'Dmi plugin registration with invalid json'() {
         given: 'a dmi plugin registration with #scenario'
             def jsonDataWithUndefinedDataLabel = '{"notAdmiPlugin":""}'
         when: 'post request is performed & registration is called with correct DMI plugin information'
@@ -95,4 +102,74 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
             response.status == HttpStatus.BAD_REQUEST.value()
     }
 
+    def 'DMI Registration: All cm-handles operations processed successfully.'() {
+        given: 'a dmi plugin registration'
+            def dmiRegistrationRequest = '{}'
+        and: 'service can register cm-handles successfully'
+            def dmiRegistrationResponse = new DmiPluginRegistrationResponse(
+                createdCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-1')],
+                updatedCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-2')],
+                removedCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-3')]
+            )
+            mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(*_) >> dmiRegistrationResponse
+        when: 'registration endpoint is invoked'
+            def response = mvc.perform(
+                post("$ncmpBasePathV1/ch")
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .content(dmiRegistrationRequest)
+            ).andReturn().response
+        then: 'response status is ok'
+            response.status == HttpStatus.OK.value()
+        and: 'the response body is empty'
+            response.getContentAsString() == ''
+
+    }
+
+    def 'DMI Registration Error Handling: #scenario.'() {
+        given: 'a dmi plugin registration'
+            def dmiRegistrationRequest = '{}'
+        and: '#scenario: service failed to register few cm-handle'
+            def dmiRegistrationResponse = new DmiPluginRegistrationResponse(
+                createdCmHandles: [createCmHandleResponse],
+                updatedCmHandles: [updateCmHandleResponse],
+                removedCmHandles: [removeCmHandleResponse]
+            )
+            mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(*_) >> dmiRegistrationResponse
+        when: 'registration endpoint is invoked'
+            def response = mvc.perform(
+                post("$ncmpBasePathV1/ch")
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .content(dmiRegistrationRequest)
+            ).andReturn().response
+        then: 'request status is internal server error'
+            response.status == HttpStatus.INTERNAL_SERVER_ERROR.value()
+        and: 'the response body is in the expected format'
+            def responseBody = jsonObjectMapper.convertJsonString(response.getContentAsString(), DmiPluginRegistrationErrorResponse)
+        and: 'contains only the failure responses'
+            responseBody.getFailedCreatedCmHandles() == expectedFailedCreatedCmHandle
+            responseBody.getFailedUpdatedCmHandles() == expectedFailedUpdateCmHandle
+            responseBody.getFailedRemovedCmHandles() == expectedFailedRemovedCmHandle
+        where:
+            scenario               | createCmHandleResponse         | updateCmHandleResponse         | removeCmHandleResponse         || expectedFailedCreatedCmHandle       | expectedFailedUpdateCmHandle        | expectedFailedRemovedCmHandle
+            'only create failed'   | failedResponse('cm-handle-1')  | successResponse('cm-handle-2') | successResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | []                                  | []
+            'only update failed'   | successResponse('cm-handle-1') | failedResponse('cm-handle-2')  | successResponse('cm-handle-3') || []                                  | [failedRestResponse('cm-handle-2')] | []
+            'only delete failed'   | successResponse('cm-handle-1') | successResponse('cm-handle-2') | failedResponse('cm-handle-3')  || []                                  | []                                  | [failedRestResponse('cm-handle-3')]
+            'all three failed'     | failedResponse('cm-handle-1')  | failedResponse('cm-handle-2')  | failedResponse('cm-handle-3')  || [failedRestResponse('cm-handle-1')] | [failedRestResponse('cm-handle-2')] | [failedRestResponse('cm-handle-3')]
+            'create update failed' | failedResponse('cm-handle-1')  | failedResponse('cm-handle-2')  | successResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | [failedRestResponse('cm-handle-2')] | []
+            'create delete failed' | failedResponse('cm-handle-1')  | successResponse('cm-handle-2') | failedResponse('cm-handle-3')  || [failedRestResponse('cm-handle-1')] | []                                  | [failedRestResponse('cm-handle-3')]
+            'update delete failed' | successResponse('cm-handle-1') | failedResponse('cm-handle-2')  | failedResponse('cm-handle-3')  || []                                  | [failedRestResponse('cm-handle-2')] | [failedRestResponse('cm-handle-3')]
+    }
+
+    def failedRestResponse(cmHandle) {
+        return new CmHandlerRegistrationErrorResponse('cmHandle': cmHandle, 'errorCode': '00', 'errorText': 'Failed')
+    }
+
+    def failedResponse(cmHandle) {
+        return CmHandleRegistrationResponse.createFailureResponse(cmHandle, new RuntimeException("Failed"))
+    }
+
+    def successResponse(cmHandle) {
+        return CmHandleRegistrationResponse.createSuccessResponse(cmHandle)
+    }
+
 }
index fd8b56b..c2a307d 100644 (file)
@@ -3,7 +3,7 @@
   "dmiModelPlugin":"service3",
   "createdCmHandles":[
     {
-      "cmHandle":"ch1(new)",
+      "cmHandle":"ch1-new",
       "cmHandleProperties":{
         "dmiProp1":"ch1-dmi1",
         "dmiProp2":"ch1-dmi2"
@@ -14,7 +14,7 @@
       }
     },
     {
-      "cmHandle":"ch2(new)",
+      "cmHandle":"ch2-new",
       "cmHandleProperties":{
         "dmiProp1":"ch2-dmi1",
         "dmiProp2":"ch2-dmi2"
@@ -27,7 +27,7 @@
   ],
   "updatedCmHandles":[
     {
-      "cmHandle":"ch3(upd)",
+      "cmHandle":"ch3-upd",
       "cmHandleProperties":{
         "dmiProp1":"ch3-dmi1"
       },
index 58a1a98..26acdbd 100644 (file)
@@ -2,7 +2,7 @@
   "dmiPlugin": "service1",
   "updatedCmHandles":[
     {
-      "cmHandle":"ch3(upd)",
+      "cmHandle":"ch3-upd",
       "cmHandleProperties":{
         "dmiProp1":"ch3-dmi1",
         "dmiProp2":null
index 395c098..a5dd7b0 100644 (file)
@@ -4,7 +4,7 @@
   "dmiModelPlugin":"service3",
   "createdCmHandles":[
     {
-      "cmHandle": "ch1(new)"
+      "cmHandle": "ch1-new"
     }
   ]
 }
index 69e9c7b..1a69e45 100755 (executable)
@@ -31,7 +31,6 @@ import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMES
 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum;
 import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -54,16 +53,17 @@ import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations;
 import org.onap.cps.ncmp.api.impl.operations.DmiOperations;
 import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
-import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandlesList;
 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse;
 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError;
 import org.onap.cps.ncmp.api.models.DmiPluginRegistration;
 import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse;
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
+import org.onap.cps.spi.exceptions.AlreadyDefinedException;
 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
 import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.spi.exceptions.SchemaSetNotFoundException;
 import org.onap.cps.spi.model.ModuleReference;
+import org.onap.cps.utils.CpsValidator;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -101,22 +101,16 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
         final DmiPluginRegistration dmiPluginRegistration) {
         dmiPluginRegistration.validateDmiPluginRegistration();
         final var dmiPluginRegistrationResponse = new DmiPluginRegistrationResponse();
-        try {
-            dmiPluginRegistrationResponse.setRemovedCmHandles(
-                parseAndRemoveCmHandlesInDmiRegistration(dmiPluginRegistration.getRemovedCmHandles()));
-            if (!dmiPluginRegistration.getCreatedCmHandles().isEmpty()) {
-                parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration);
-            }
-            if (!dmiPluginRegistration.getUpdatedCmHandles().isEmpty()) {
-                dmiPluginRegistrationResponse.setUpdatedCmHandles(
-                    networkCmProxyDataServicePropertyHandler
-                        .updateCmHandleProperties(dmiPluginRegistration.getUpdatedCmHandles()));
-            }
-        } catch (final JsonProcessingException | DataNodeNotFoundException e) {
-            final String errorMessage = String.format(
-                    "Error occurred while processing the CM-handle registration request, caused by : [%s]",
-                    e.getMessage());
-            throw new DataValidationException(errorMessage, e.getMessage(), e);
+        dmiPluginRegistrationResponse.setRemovedCmHandles(
+            parseAndRemoveCmHandlesInDmiRegistration(dmiPluginRegistration.getRemovedCmHandles()));
+        if (!dmiPluginRegistration.getCreatedCmHandles().isEmpty()) {
+            dmiPluginRegistrationResponse.setCreatedCmHandles(
+                parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration));
+        }
+        if (!dmiPluginRegistration.getUpdatedCmHandles().isEmpty()) {
+            dmiPluginRegistrationResponse.setUpdatedCmHandles(
+                networkCmProxyDataServicePropertyHandler
+                    .updateCmHandleProperties(dmiPluginRegistration.getUpdatedCmHandles()));
         }
         return dmiPluginRegistrationResponse;
     }
@@ -127,7 +121,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
                                                         final String acceptParamInHeader,
                                                         final String optionsParamInQuery,
                                                         final String topicParamInQuery) {
-
+        CpsValidator.validateNameCharacters(cmHandleId);
         return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, acceptParamInHeader,
                 DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, optionsParamInQuery, topicParamInQuery);
     }
@@ -138,6 +132,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
                                                                final String acceptParamInHeader,
                                                                final String optionsParamInQuery,
                                                                final String topicParamInQuery) {
+        CpsValidator.validateNameCharacters(cmHandleId);
         return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, acceptParamInHeader,
                 DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING, optionsParamInQuery, topicParamInQuery);
     }
@@ -148,6 +143,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
                                                                final OperationEnum operation,
                                                                final String requestData,
                                                                final String dataType) {
+        CpsValidator.validateNameCharacters(cmHandleId);
         return handleResponse(
                 dmiDataOperations.writeResourceDataPassThroughRunningFromDmi(cmHandleId, resourceIdentifier, operation,
                         requestData, dataType), operation);
@@ -156,6 +152,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
 
     @Override
     public Collection<ModuleReference> getYangResourcesModuleReferences(final String cmHandleId) {
+        CpsValidator.validateNameCharacters(cmHandleId);
         return cpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId);
     }
 
@@ -178,6 +175,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
      */
     @Override
     public NcmpServiceCmHandle getNcmpServiceCmHandle(final String cmHandleId) {
+        CpsValidator.validateNameCharacters(cmHandleId);
         final NcmpServiceCmHandle ncmpServiceCmHandle = new NcmpServiceCmHandle();
         final YangModelCmHandle yangModelCmHandle =
             yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId);
@@ -214,14 +212,19 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
      * THis method registers a cm handle and initiates modules sync.
      *
      * @param dmiPluginRegistration dmi plugin registration information.
-     * @throws JsonProcessingException thrown if json is malformed or missing.
+     * @return cm-handle registration response for create cm-handle requests.
      */
-    public void parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(
-        final DmiPluginRegistration dmiPluginRegistration) throws JsonProcessingException {
-        final YangModelCmHandlesList createdYangModelCmHandlesList =
-            getUpdatedYangModelCmHandlesList(dmiPluginRegistration,
-                dmiPluginRegistration.getCreatedCmHandles());
-        registerAndSyncNewCmHandles(createdYangModelCmHandlesList);
+    public List<CmHandleRegistrationResponse> parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(
+        final DmiPluginRegistration dmiPluginRegistration) {
+        return dmiPluginRegistration.getCreatedCmHandles().stream()
+            .map(cmHandle ->
+                YangModelCmHandle.toYangModelCmHandle(
+                    dmiPluginRegistration.getDmiPlugin(),
+                    dmiPluginRegistration.getDmiDataPlugin(),
+                    dmiPluginRegistration.getDmiModelPlugin(), cmHandle)
+            )
+            .map(this::registerAndSyncNewCmHandle)
+            .collect(Collectors.toList());
     }
 
     private static Object handleResponse(final ResponseEntity<?> responseEntity, final OperationEnum operation) {
@@ -234,23 +237,23 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
         }
     }
 
-    private YangModelCmHandlesList getUpdatedYangModelCmHandlesList(
-        final DmiPluginRegistration dmiPluginRegistration,
-        final List<NcmpServiceCmHandle> updatedCmHandles) {
-        return YangModelCmHandlesList.toYangModelCmHandlesList(
-            dmiPluginRegistration.getDmiPlugin(),
-            dmiPluginRegistration.getDmiDataPlugin(),
-            dmiPluginRegistration.getDmiModelPlugin(),
-            updatedCmHandles);
-    }
-
-    private void registerAndSyncNewCmHandles(final YangModelCmHandlesList yangModelCmHandlesList) {
-        final String cmHandleJsonData = jsonObjectMapper.asJsonString(yangModelCmHandlesList);
-        cpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
+    private CmHandleRegistrationResponse registerAndSyncNewCmHandle(final YangModelCmHandle yangModelCmHandle) {
+        try {
+            CpsValidator.validateNameCharacters(yangModelCmHandle.getId());
+            final String cmHandleJsonData = String.format("{\"cm-handles\":[%s]}",
+                jsonObjectMapper.asJsonString(yangModelCmHandle));
+            cpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
                 cmHandleJsonData, NO_TIMESTAMP);
-
-        for (final YangModelCmHandle yangModelCmHandle : yangModelCmHandlesList.getYangModelCmHandles()) {
             syncModulesAndCreateAnchor(yangModelCmHandle);
+            return CmHandleRegistrationResponse.createSuccessResponse(yangModelCmHandle.getId());
+        } catch (final AlreadyDefinedException alreadyDefinedException) {
+            return CmHandleRegistrationResponse.createFailureResponse(
+                yangModelCmHandle.getId(), RegistrationError.CM_HANDLE_ALREADY_EXIST);
+        } catch (final DataValidationException dataValidationException) {
+            return CmHandleRegistrationResponse.createFailureResponse(yangModelCmHandle.getId(),
+                RegistrationError.CM_HANDLE_INVALID_ID);
+        } catch (final Exception exception) {
+            return CmHandleRegistrationResponse.createFailureResponse(yangModelCmHandle.getId(), exception);
         }
     }
 
@@ -259,12 +262,13 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
         createAnchor(yangModelCmHandle);
     }
 
-    private List<CmHandleRegistrationResponse> parseAndRemoveCmHandlesInDmiRegistration(
+    protected List<CmHandleRegistrationResponse> parseAndRemoveCmHandlesInDmiRegistration(
         final List<String> tobeRemovedCmHandles) {
         final List<CmHandleRegistrationResponse> cmHandleRegistrationResponses =
             new ArrayList<>(tobeRemovedCmHandles.size());
         for (final String cmHandle : tobeRemovedCmHandles) {
             try {
+                CpsValidator.validateNameCharacters(cmHandle);
                 deleteSchemaSetWithCascade(cmHandle);
                 cpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
                     "/dmi-registry/cm-handles[@id='" + cmHandle + "']", NO_TIMESTAMP);
@@ -274,8 +278,13 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
                     cmHandle, dataNodeNotFoundException.getMessage());
                 cmHandleRegistrationResponses.add(CmHandleRegistrationResponse
                     .createFailureResponse(cmHandle, RegistrationError.CM_HANDLE_DOES_NOT_EXIST));
+            } catch (final DataValidationException dataValidationException) {
+                log.error("Unable to de-register cm-handle id: {}, caused by: {}",
+                    cmHandle, dataValidationException.getMessage());
+                cmHandleRegistrationResponses.add(CmHandleRegistrationResponse
+                    .createFailureResponse(cmHandle, RegistrationError.CM_HANDLE_INVALID_ID));
             } catch (final Exception exception) {
-                log.error("Unable to de-register cm-handleIdd : {} , caused by : {}",
+                log.error("Unable to de-register cm-handle id : {} , caused by : {}",
                     cmHandle, exception.getMessage());
                 cmHandleRegistrationResponses.add(
                     CmHandleRegistrationResponse.createFailureResponse(cmHandle, exception));
index c838a75..ff79f87 100644 (file)
@@ -45,8 +45,10 @@ import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationErr
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
 import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
+import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.model.DataNodeBuilder;
+import org.onap.cps.utils.CpsValidator;
 import org.springframework.stereotype.Service;
 
 @Slf4j
@@ -72,6 +74,7 @@ public class NetworkCmProxyDataServicePropertyHandler {
         for (final NcmpServiceCmHandle ncmpServiceCmHandle : ncmpServiceCmHandles) {
             final String cmHandle = ncmpServiceCmHandle.getCmHandleID();
             try {
+                CpsValidator.validateNameCharacters(cmHandle);
                 final String cmHandleXpath = String.format(CM_HANDLE_XPATH_TEMPLATE, cmHandle);
                 final DataNode existingCmHandleDataNode =
                         cpsDataService.getDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandleXpath,
@@ -83,8 +86,14 @@ public class NetworkCmProxyDataServicePropertyHandler {
                     cmHandle, e.getMessage());
                 cmHandleRegistrationResponses.add(CmHandleRegistrationResponse
                     .createFailureResponse(cmHandle, RegistrationError.CM_HANDLE_DOES_NOT_EXIST));
+            } catch (final DataValidationException e) {
+                log.error("Unable to update cm handle : {}, caused by : {}",
+                    cmHandle, e.getMessage());
+                cmHandleRegistrationResponses.add(
+                    CmHandleRegistrationResponse.createFailureResponse(cmHandle,
+                        RegistrationError.CM_HANDLE_INVALID_ID));
             } catch (final Exception exception) {
-                log.error("Unable to update dataNode for cmHandleId : {} , caused by : {}",
+                log.error("Unable to update cmHandle : {} , caused by : {}",
                     cmHandle, exception.getMessage());
                 cmHandleRegistrationResponses.add(
                     CmHandleRegistrationResponse.createFailureResponse(cmHandle, exception));
index 47062b3..e46b9e3 100644 (file)
@@ -21,6 +21,8 @@
 
 package org.onap.cps.ncmp.api.impl.yangmodels;
 
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.Strings;
 import java.util.ArrayList;
@@ -41,6 +43,7 @@ import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
 @Getter
 @Setter
 @NoArgsConstructor
+@JsonInclude(Include.NON_NULL)
 public class YangModelCmHandle {
 
     private String id;
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandlesList.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandlesList.java
deleted file mode 100644 (file)
index 261a018..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2022 Nordix Foundation
- *  ================================================================================
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- *
- *  SPDX-License-Identifier: Apache-2.0
- *  ============LICENSE_END=========================================================
- */
-
-package org.onap.cps.ncmp.api.impl.yangmodels;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import lombok.Getter;
-import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
-
-@Getter
-public class YangModelCmHandlesList {
-
-    @JsonProperty("cm-handles")
-    private final List<YangModelCmHandle> yangModelCmHandles = new ArrayList<>();
-
-    /**
-     * Create a YangModelCmHandleList given all service names and a collection of cmHandles.
-     * @param dmiServiceName the dmi service name
-     * @param dmiDataServiceName the dmi data service name
-     * @param dmiModelServiceName the dmi model service name
-     * @param ncmpServiceCmHandles cm handles rest model
-     * @return instance of YangModelCmHandleList
-     */
-    public static YangModelCmHandlesList toYangModelCmHandlesList(final String dmiServiceName,
-                                                                  final String dmiDataServiceName,
-                                                                  final String dmiModelServiceName,
-                                                                  final Collection<NcmpServiceCmHandle>
-                                                            ncmpServiceCmHandles) {
-        final YangModelCmHandlesList yangModelCmHandlesList = new YangModelCmHandlesList();
-        for (final NcmpServiceCmHandle ncmpServiceCmHandle : ncmpServiceCmHandles) {
-            final YangModelCmHandle yangModelCmHandle =
-                YangModelCmHandle.toYangModelCmHandle(
-                    dmiServiceName,
-                    dmiDataServiceName,
-                    dmiModelServiceName,
-                    ncmpServiceCmHandle);
-            yangModelCmHandlesList.add(yangModelCmHandle);
-        }
-        return yangModelCmHandlesList;
-    }
-
-    /**
-     * Add a yangModelCmHandle.
-     *
-     * @param yangModelCmHandle the yangModelCmHandle to add
-     */
-    public void add(final YangModelCmHandle yangModelCmHandle) {
-        yangModelCmHandles.add(yangModelCmHandle);
-    }
-}
index e183ed1..1da2aa9 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Bell Canada
+ *  Modifications Copyright (C) 2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -77,7 +78,8 @@ public class CmHandleRegistrationResponse {
     public enum RegistrationError {
         UNKNOWN_ERROR("00", "Unknown error"),
         CM_HANDLE_ALREADY_EXIST("01", "cm-handle already exists"),
-        CM_HANDLE_DOES_NOT_EXIST("02", "cm-handle does not exist");
+        CM_HANDLE_DOES_NOT_EXIST("02", "cm-handle does not exist"),
+        CM_HANDLE_INVALID_ID("03", "cm-handle has an invalid character(s) in id");
 
         public final String errorCode;
         public final String errorText;
index ce2f3e6..8a3d264 100644 (file)
@@ -20,6 +20,7 @@
 
 package org.onap.cps.ncmp.api.models;
 
+import java.util.Collections;
 import java.util.List;
 import lombok.Data;
 import lombok.NoArgsConstructor;
@@ -27,7 +28,7 @@ import lombok.NoArgsConstructor;
 @Data
 @NoArgsConstructor
 public class DmiPluginRegistrationResponse {
-    private List<CmHandleRegistrationResponse> createdCmHandles;
-    private List<CmHandleRegistrationResponse> updatedCmHandles;
-    private List<CmHandleRegistrationResponse> removedCmHandles;
+    private List<CmHandleRegistrationResponse> createdCmHandles = Collections.emptyList();
+    private List<CmHandleRegistrationResponse> updatedCmHandles = Collections.emptyList();
+    private List<CmHandleRegistrationResponse> removedCmHandles = Collections.emptyList();
 }
\ No newline at end of file
index 23d2438..cb4d5ef 100644 (file)
@@ -21,9 +21,8 @@
 
 package org.onap.cps.ncmp.api.impl
 
-import com.fasterxml.jackson.core.JsonProcessingException
 import com.fasterxml.jackson.databind.ObjectMapper
-import java.util.function.Predicate
+import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
 import org.onap.cps.api.CpsAdminService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsModuleService
@@ -34,6 +33,7 @@ import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever
 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse
 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
+import org.onap.cps.spi.exceptions.AlreadyDefinedException
 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
 import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.spi.exceptions.SchemaSetNotFoundException
@@ -42,7 +42,10 @@ import spock.lang.Shared
 import spock.lang.Specification
 
 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_DOES_NOT_EXIST
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_ALREADY_EXIST
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_INVALID_ID
 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.UNKNOWN_ERROR
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status
 import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED
 
 class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
@@ -75,73 +78,190 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiRegistration)
             // Spock validated invocation order between multiple then blocks
         then: 'cm-handles are removed first'
-            1 * mockCpsDataService.deleteListOrListElement(*_)
+            1 * objectUnderTest.parseAndRemoveCmHandlesInDmiRegistration(*_)
         then: 'cm-handles are created'
-            1 * mockCpsDataService.saveListElements(*_)
+            1 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(*_)
         then: 'cm-handles are updated'
             1 * mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_)
     }
 
-    def 'Register or re-register a DMI Plugin for the given cm-handle(s) with #scenario process.'() {
-        given: 'a registration'
-            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
-            ncmpServiceCmHandle.cmHandleID = '123'
-            ncmpServiceCmHandle.dmiProperties = [dmiProp1: 'dmiValue1', dmiProp2: 'dmiValue2']
-            ncmpServiceCmHandle.publicProperties = [publicProp1: 'publicValue1', publicProp2: 'publicValue2']
-            dmiPluginRegistration.createdCmHandles = createdCmHandles
-            dmiPluginRegistration.updatedCmHandles = updatedCmHandles
-            dmiPluginRegistration.removedCmHandles = removedCmHandles
-            def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","dmi-data-service-name":null,"dmi-model-service-name":null,' +
-                '"additional-properties":[{"name":"dmiProp1","value":"dmiValue1"},{"name":"dmiProp2","value":"dmiValue2"}],' +
-                '"public-properties":[{"name":"publicProp1","value":"publicValue1"},{"name":"publicProp2","value":"publicValue2"}]' +
-                '}]}'
-        when: 'registration is updated and modules are synced'
+    def 'DMI Registration: Response from all operations types are in response'() {
+        given: 'a registration with operations of all three types'
+            def dmiRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
+            dmiRegistration.setCreatedCmHandles([new NcmpServiceCmHandle(cmHandleID: 'cmhandle-1', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
+            dmiRegistration.setUpdatedCmHandles([new NcmpServiceCmHandle(cmHandleID: 'cmhandle-2', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
+            dmiRegistration.setRemovedCmHandles(['cmhandle-2'])
+        and: 'update cm-handles can be processed successfully'
+            def updateResponses = [CmHandleRegistrationResponse.createSuccessResponse('cmhandle-2')]
+            mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_) >> updateResponses
+        and: 'create cm-handles can be processed successfully'
+            def createdResponses = [CmHandleRegistrationResponse.createSuccessResponse('cmhandle-1')]
+            objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(*_) >> createdResponses
+        and: 'delete cm-handles can be processed successfully'
+            def removeResponses = [CmHandleRegistrationResponse.createSuccessResponse('cmhandle-3')]
+            objectUnderTest.parseAndRemoveCmHandlesInDmiRegistration(*_) >> removeResponses
+        when: 'registration is processed'
+            def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiRegistration)
+        then: 'response has values from all operations'
+            response.getRemovedCmHandles() == removeResponses
+            response.getCreatedCmHandles() == createdResponses
+            response.getUpdatedCmHandles() == updateResponses
+
+
+    }
+
+    def 'Create CM-handle Validation: Registration with valid Service names: #scenario'() {
+        given: 'a registration '
+            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
+                dmiDataPlugin: dmiDataPlugin)
+            dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
+        when: 'update registration and sync module is called with correct DMI plugin information'
             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
-        then: 'save list elements is invoked with the expected parameters'
-            expectedCallsToSaveNode * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
-                '/dmi-registry', expectedJsonData, noTimestamp)
-        and: 'update data node leaves is called with correct parameters'
-            expectedCallsToUpdateCmHandleProperty * mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(updatedCmHandles)
-        and: 'delete schema set is invoked with the correct parameters'
-            expectedCallsToDeleteSchemaSetAndListElement * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'cmHandle001', CASCADE_DELETE_ALLOWED)
-        and: 'delete list or list element is invoked with the correct parameters'
-            expectedCallsToDeleteSchemaSetAndListElement * mockCpsDataService.deleteListOrListElement('NCMP-Admin',
-                'ncmp-dmi-registry', "/dmi-registry/cm-handles[@id='cmHandle001']", noTimestamp)
+        then: 'create cm handles registration and sync modules is called with the correct plugin information'
+            1 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration)
         where:
-            scenario                    | createdCmHandles      | updatedCmHandles      | removedCmHandles || expectedCallsToSaveNode | expectedCallsToDeleteSchemaSetAndListElement | expectedCallsToUpdateCmHandleProperty
-            'create'                    | [ncmpServiceCmHandle] | []                    | []               || 1                       | 0                                            | 0
-            'update'                    | []                    | [ncmpServiceCmHandle] | []               || 0                       | 0                                            | 1
-            'delete'                    | []                    | []                    | cmHandlesArray   || 0                       | 1                                            | 0
-            'create, update and delete' | [ncmpServiceCmHandle] | [ncmpServiceCmHandle] | cmHandlesArray   || 1                       | 1                                            | 1
-            'no valid data'             | []                    | []                    | []               || 0                       | 0                                            | 0
+            scenario                          | dmiPlugin  | dmiModelPlugin | dmiDataPlugin
+            'combined DMI plugin'             | 'service1' | ''             | ''
+            'data & model DMI plugins'        | ''         | 'service1'     | 'service2'
+            'data & model using same service' | ''         | 'service1'     | 'service1'
     }
 
-    def 'Create CM-Handle: Register a DMI Plugin for the given cm-handle(s) without DMI properties.'() {
+    def 'Create CM-handle Validation: Invalid DMI plugin service name with #scenario'() {
+        given: 'a registration '
+            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
+                dmiDataPlugin: dmiDataPlugin)
+            dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
+        when: 'registration is called with incorrect DMI plugin information'
+            objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+        then: 'a DMI Request Exception is thrown with correct message details'
+            def exceptionThrown = thrown(DmiRequestException.class)
+            assert exceptionThrown.getMessage().contains(expectedMessageDetails)
+        and: 'registration is not called'
+            0 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration)
+        where:
+            scenario                         | dmiPlugin  | dmiModelPlugin | dmiDataPlugin || expectedMessageDetails
+            'empty DMI plugins'              | ''         | ''             | ''            || 'No DMI plugin service names'
+            'blank DMI plugins'              | ' '        | ' '            | ' '           || 'No DMI plugin service names'
+            'null DMI plugins'               | null       | null           | null          || 'No DMI plugin service names'
+            'all DMI plugins'                | 'service1' | 'service2'     | 'service3'    || 'Cannot register combined plugin service name and other service names'
+            '(combined)DMI and Data Plugin'  | 'service1' | ''             | 'service2'    || 'Cannot register combined plugin service name and other service names'
+            '(combined)DMI and model Plugin' | 'service1' | 'service2'     | ''            || 'Cannot register combined plugin service name and other service names'
+            'only model DMI plugin'          | ''         | 'service1'     | ''            || 'Cannot register just a Data or Model plugin service name'
+            'only data DMI plugin'           | ''         | ''             | 'service1'    || 'Cannot register just a Data or Model plugin service name'
+    }
+
+    def 'Create CM-Handle Successfully: #scenario.'() {
         given: 'a registration without cm-handle properties'
-            NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
-            ncmpServiceCmHandle.cmHandleID = '123'
-            ncmpServiceCmHandle.dmiProperties = Collections.emptyMap()
-            ncmpServiceCmHandle.publicProperties = Collections.emptyMap()
-            dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
-            def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","dmi-data-service-name":null,"dmi-model-service-name":null,"additional-properties":[],"public-properties":[]}]}'
+            dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleID: 'cmhandle', dmiProperties: dmiProperties, publicProperties: publicProperties)]
         when: 'registration is updated'
-            objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
-        then: 'save list elements is invoked with the expected parameters'
-            1 * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
-                '/dmi-registry', expectedJsonData, noTimestamp)
+            def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+        then: 'a successful response is received'
+            response.getCreatedCmHandles().size() == 1
+            with(response.getCreatedCmHandles().get(0)) {
+                assert it.status == Status.SUCCESS
+                assert it.cmHandle == 'cmhandle'
+            }
+        and: 'save list elements is invoked with the expected parameters'
+            interaction {
+                def expectedJsonData = """{"cm-handles":[{"id":"cmhandle","dmi-service-name":"my-server","additional-properties":$expectedDmiProperties,"public-properties":$expectedPublicProperties}]}"""
+                1 * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
+                    '/dmi-registry', expectedJsonData, noTimestamp)
+            }
+        then: 'model sync is invoked with expected parameters'
+            1 * objectUnderTest.syncModulesAndCreateAnchor(_) >> { YangModelCmHandle yangModelCmHandle ->
+                {
+                    assert yangModelCmHandle.id == 'cmhandle'
+                    assert yangModelCmHandle.dmiServiceName == 'my-server'
+                    assert spiedJsonObjectMapper.asJsonString(yangModelCmHandle.getPublicProperties()) == expectedPublicProperties
+                    assert spiedJsonObjectMapper.asJsonString(yangModelCmHandle.getDmiProperties()) == expectedDmiProperties
+
+                }
+            }
+        where:
+            scenario                          | dmiProperties            | publicProperties               || expectedDmiProperties                      | expectedPublicProperties
+            'with dmi & public properties'    | ['dmi-key': 'dmi-value'] | ['public-key': 'public-value'] || '[{"name":"dmi-key","value":"dmi-value"}]' | '[{"name":"public-key","value":"public-value"}]'
+            'with only public properties'     | [:]                      | ['public-key': 'public-value'] || '[]'                                       | '[{"name":"public-key","value":"public-value"}]'
+            'with only dmi properties'        | ['dmi-key': 'dmi-value'] | [:]                            || '[{"name":"dmi-key","value":"dmi-value"}]' | '[]'
+            'without dmi & public properties' | [:]                      | [:]                            || '[]'                                       | '[]'
+
     }
 
-    def 'Create CM-Handle: Register a DMI Plugin for a given cm-handle(s) with JSON processing errors during process.'() {
-        given: 'a registration without cm-handle properties '
-            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'some-plugin')
-            dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
-        and: 'an json processing exception occurs'
-            spiedJsonObjectMapper.asJsonString(_) >> { throw (new JsonProcessingException('')) }
-        when: 'registration is updated and modules are synced'
-            objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
-        then: 'a data validation exception is thrown'
-            thrown(DataValidationException)
+    def 'Create CM-Handle Multiple Requests: All cm-handles creation requests are processed'() {
+        given: 'a registration with three cm-handles to be created'
+            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
+                createdCmHandles: [new NcmpServiceCmHandle(cmHandleID: 'cmhandle1'),
+                                   new NcmpServiceCmHandle(cmHandleID: 'cmhandle2'),
+                                   new NcmpServiceCmHandle(cmHandleID: 'cmhandle3')])
+        and: 'cm-handle creation is successful for 1st and 3rd; failed for 2nd'
+            mockCpsDataService.saveListElements(_, _, _, _, _) >> {} >> { throw new RuntimeException("Failed") } >> {}
+        when: 'registration is updated to create cm-handles'
+            def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+        then: 'a response is received for all cm-handles'
+            response.getCreatedCmHandles().size() == 3
+        and: '1st and 3rd cm-handle are created successfully'
+            with(response.getCreatedCmHandles().get(0)) {
+                assert it.status == Status.SUCCESS
+                assert it.cmHandle == 'cmhandle1'
+            }
+            with(response.getCreatedCmHandles().get(2)) {
+                assert it.status == Status.SUCCESS
+                assert it.cmHandle == 'cmhandle3'
+            }
+        and: '2nd cm-handle creation fails'
+            with(response.getCreatedCmHandles().get(1)) {
+                assert it.status == Status.FAILURE
+                assert it.registrationError == UNKNOWN_ERROR
+                assert it.errorText == 'Failed'
+                assert it.cmHandle == 'cmhandle2'
+            }
+    }
+
+    def 'Create CM-Handle Error Handling: Registration fails: #scenario'() {
+        given: 'a registration without cm-handle properties'
+            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
+            dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleID: cmHandleId)]
+        and: 'cm-handler registration fails: #scenario'
+            mockCpsDataService.saveListElements(_, _, _, _, _) >> { throw exception }
+        when: 'registration is updated'
+            def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+        then: 'a failure response is received'
+            response.getCreatedCmHandles().size() == 1
+            with(response.getCreatedCmHandles().get(0)) {
+                assert it.status == Status.FAILURE
+                assert it.cmHandle ==  cmHandleId
+                assert it.registrationError == expectedError
+                assert it.errorText == expectedErrorText
+            }
+        and: 'model-sync is not invoked'
+            0 * objectUnderTest.syncModulesAndCreateAnchor(_)
+        where:
+            scenario                                        | cmHandleId             | exception                                               || expectedError           | expectedErrorText
+            'cm-handle already exist'                       | 'cmhandle'             | new AlreadyDefinedException('', new RuntimeException()) || CM_HANDLE_ALREADY_EXIST | 'cm-handle already exists'
+            'cm-handle has invalid name'                    | 'cm handle with space' | new DataValidationException("", "")                     || CM_HANDLE_INVALID_ID    | 'cm-handle has an invalid character(s) in id'
+            'unknown exception while registering cm-handle' | 'cmhandle'             | new RuntimeException('Failed')                          || UNKNOWN_ERROR           | 'Failed'
+    }
+
+    def 'Create CM-Handle Error Handling: Model Sync fails'() {
+        given: 'objects under test without disabled model sync'
+            def objectUnderTest = getObjectUnderTest()
+        and: 'a registration without cm-handle properties'
+            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
+            dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleID: 'cmhandle')]
+        and: 'cm-handler models sync fails'
+            objectUnderTest.syncModulesAndCreateAnchor(*_) >> { throw new RuntimeException('Model-Sync failed') }
+        when: 'registration is updated'
+            def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+        then: 'a failure response is received'
+            response.getCreatedCmHandles().size() == 1
+            with(response.getCreatedCmHandles().get(0)) {
+                assert it.status == Status.FAILURE
+                assert it.cmHandle == 'cmhandle'
+                assert it.registrationError == UNKNOWN_ERROR
+                assert it.errorText == 'Model-Sync failed'
+            }
+        and: 'cm-handle is registered'
+            1 * mockCpsDataService.saveListElements(*_)
     }
 
     def 'Update CM-Handle: Update Operation Response is added to the response'() {
@@ -151,12 +271,13 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
         and: 'cm-handle updates can be processed successfully'
             def updateOperationResponse = [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-1'),
                                            CmHandleRegistrationResponse.createFailureResponse('cm-handle-2', new Exception("Failed")),
-                                           CmHandleRegistrationResponse.createFailureResponse('cm-handle-3', CM_HANDLE_DOES_NOT_EXIST)]
+                                           CmHandleRegistrationResponse.createFailureResponse('cm-handle-3', CM_HANDLE_DOES_NOT_EXIST),
+                                           CmHandleRegistrationResponse.createFailureResponse('cm handle 4', CM_HANDLE_INVALID_ID)]
             mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(_) >> updateOperationResponse
         when: 'registration is updated'
             def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
         then: 'the response contains updateOperationResponse'
-            assert response.getUpdatedCmHandles().size() == 3
+            assert response.getUpdatedCmHandles().size() == 4
             assert response.getUpdatedCmHandles().containsAll(updateOperationResponse)
     }
 
@@ -174,7 +295,7 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
         and: 'successful response is received'
             assert response.getRemovedCmHandles().size() == 1
             with(response.getRemovedCmHandles().get(0)) {
-                assert it.status == CmHandleRegistrationResponse.Status.SUCCESS
+                assert it.status == Status.SUCCESS
                 assert it.cmHandle == 'cmhandle'
             }
         where:
@@ -195,16 +316,19 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
             response.getRemovedCmHandles().size() == 3
         and: '1st and 3rd cm-handle deletes successfully'
             with(response.getRemovedCmHandles().get(0)) {
-                assert it.status == CmHandleRegistrationResponse.Status.SUCCESS
+                assert it.status == Status.SUCCESS
+                assert it.cmHandle == 'cmhandle1'
             }
             with(response.getRemovedCmHandles().get(2)) {
-                assert it.status == CmHandleRegistrationResponse.Status.SUCCESS
+                assert it.status == Status.SUCCESS
+                assert it.cmHandle == 'cmhandle3'
             }
-        and: '2nd cmhandle deletion fails'
+        and: '2nd cm-handle deletion fails'
             with(response.getRemovedCmHandles().get(1)) {
-                assert it.status == CmHandleRegistrationResponse.Status.FAILURE
+                assert it.status == Status.FAILURE
                 assert it.registrationError == UNKNOWN_ERROR
                 assert it.errorText == 'Failed'
+                assert it.cmHandle == 'cmhandle2'
             }
     }
 
@@ -223,7 +347,7 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
         and: 'a failure response is received'
             assert response.getRemovedCmHandles().size() == 1
             with(response.getRemovedCmHandles().get(0)) {
-                assert it.status == CmHandleRegistrationResponse.Status.FAILURE
+                assert it.status == Status.FAILURE
                 assert it.cmHandle == 'cmhandle'
                 assert it.errorText == 'Failed'
                 assert it.registrationError == UNKNOWN_ERROR
@@ -243,61 +367,26 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
         and: 'a failure response is received'
             assert response.getRemovedCmHandles().size() == 1
             with(response.getRemovedCmHandles().get(0)) {
-                assert it.status == CmHandleRegistrationResponse.Status.FAILURE
+                assert it.status == Status.FAILURE
                 assert it.cmHandle == 'cmhandle'
                 assert it.registrationError == expectedError
                 assert it.errorText == expectedErrorText
             }
         where:
-            scenario                   | deleteListElementException                | expectedError            | expectedErrorText
-            'cm-handle does not exist' | new DataNodeNotFoundException("", "", "") | CM_HANDLE_DOES_NOT_EXIST | 'cm-handle does not exist'
-            'an unexpected exception'  | new RuntimeException("Failed")            | UNKNOWN_ERROR            | 'Failed'
-    }
-
-    def 'Create CM-handle Validation: Registration with valid Service names: #scenario'() {
-        given: 'a registration '
-            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
-                dmiDataPlugin: dmiDataPlugin)
-            dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
-        when: 'update registration and sync module is called with correct DMI plugin information'
-            objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
-        then: 'create cm handles registration and sync modules is called with the correct plugin information'
-            1 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration)
-        where:
-            scenario                          | dmiPlugin  | dmiModelPlugin | dmiDataPlugin
-            'combined DMI plugin'             | 'service1' | ''             | ''
-            'data & model DMI plugins'        | ''         | 'service1'     | 'service2'
-            'data & model using same service' | ''         | 'service1'     | 'service1'
-    }
-
-    def 'Create CM-handle Error Handling: Invalid DMI plugin service name with #scenario'() {
-        given: 'a registration '
-            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
-                dmiDataPlugin: dmiDataPlugin)
-            dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
-        when: 'registration is called with incorrect DMI plugin information'
-            objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
-        then: 'a DMI Request Exception is thrown with correct message details'
-            def exceptionThrown = thrown(DmiRequestException.class)
-            assert exceptionThrown.getMessage().contains(expectedMessageDetails)
-        and: 'registration is not called'
-            0 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration)
-        where:
-            scenario                         | dmiPlugin  | dmiModelPlugin | dmiDataPlugin || expectedMessageDetails
-            'empty DMI plugins'              | ''         | ''             | ''            || 'No DMI plugin service names'
-            'blank DMI plugins'              | ' '        | ' '            | ' '           || 'No DMI plugin service names'
-            'null DMI plugins'               | null       | null           | null          || 'No DMI plugin service names'
-            'all DMI plugins'                | 'service1' | 'service2'     | 'service3'    || 'Cannot register combined plugin service name and other service names'
-            '(combined)DMI and Data Plugin'  | 'service1' | ''             | 'service2'    || 'Cannot register combined plugin service name and other service names'
-            '(combined)DMI and model Plugin' | 'service1' | 'service2'     | ''            || 'Cannot register combined plugin service name and other service names'
-            'only model DMI plugin'          | ''         | 'service1'     | ''            || 'Cannot register just a Data or Model plugin service name'
-            'only data DMI plugin'           | ''         | ''             | 'service1'    || 'Cannot register just a Data or Model plugin service name'
+            scenario                     | cmHandleId             | deleteListElementException                ||  expectedError           | expectedErrorText
+            'cm-handle does not exist'   | 'cmhandle'             | new DataNodeNotFoundException("", "", "") || CM_HANDLE_DOES_NOT_EXIST | 'cm-handle does not exist'
+            'cm-handle has invalid name' | 'cm handle with space' | new DataValidationException("", "")       || CM_HANDLE_INVALID_ID     | 'cm-handle has an invalid character(s) in id'
+            'an unexpected exception'    | 'cmhandle'             | new RuntimeException("Failed")            || UNKNOWN_ERROR            | 'Failed'
     }
 
     def getObjectUnderTestWithModelSyncDisabled() {
-        def objectUnderTest = Spy(new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations,
-            mockCpsModuleService, mockCpsAdminService, mockNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever))
+        def objectUnderTest = getObjectUnderTest()
         objectUnderTest.syncModulesAndCreateAnchor(*_) >> null
         return objectUnderTest
     }
+
+    def getObjectUnderTest() {
+        return Spy(new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations,
+            mockCpsModuleService, mockCpsAdminService, mockNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever))
+    }
 }
index 06b2032..bf5bb73 100644 (file)
@@ -289,9 +289,9 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
 
     def 'Getting Yang Resources.'() {
         when: 'yang resources is called'
-            objectUnderTest.getYangResourcesModuleReferences('some cm handle')
+            objectUnderTest.getYangResourcesModuleReferences('some-cm-handle')
         then: 'CPS module services is invoked for the correct dataspace and cm handle'
-            1 * mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some cm handle')
+            1 * mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cm-handle')
     }
 
     def 'Get cm handle identifiers for the given module names.'() {
index f6264f4..7aacbda 100644 (file)
 
 package org.onap.cps.ncmp.api.impl
 
+import org.onap.cps.spi.exceptions.DataValidationException
+
 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_DOES_NOT_EXIST
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_INVALID_ID
 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.UNKNOWN_ERROR
 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status
 
@@ -118,7 +121,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
             'no original properties'              | []                        || 0
     }
 
-    def 'Exception thrown when we try to update cmHandle'() {
+    def '#scenario error leads to #exception when we try to update cmHandle'() {
         given: 'cm handles request'
             def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleID: cmHandleId, publicProperties: [:], dmiProperties: [:])]
         and: 'data node cannot be found'
@@ -135,9 +138,10 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
                 assert it.errorText == expectedErrorText
             }
         where:
-            scenario                  | exception                                                        || expectedError            | expectedErrorText
-            'cmhandle does not exist' | new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry') || CM_HANDLE_DOES_NOT_EXIST | 'cm-handle does not exist'
-            'unexpected error'        | new RuntimeException('Failed')                                   || UNKNOWN_ERROR            | 'Failed'
+            scenario                   | cmHandleId               | exception                                                                                           || expectedError            | expectedErrorText
+            'Cm Handle does not exist' | 'cmHandleId'             | new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry')                                    || CM_HANDLE_DOES_NOT_EXIST | 'cm-handle does not exist'
+            'Unknown'                  | 'cmHandleId'             | new RuntimeException('Failed')                                                                      || UNKNOWN_ERROR            | 'Failed'
+            'Invalid cm handle id'     | 'cmHandleId with spaces' | new DataValidationException('Name Validation Error.', cmHandleId + 'contains an invalid character') || CM_HANDLE_INVALID_ID     | 'cm-handle has an invalid character(s) in id'
     }
 
     def 'Multiple update operations in a single request'() {
index c5ef2f4..4476998 100644 (file)
@@ -26,8 +26,8 @@ import spock.lang.Specification
 
 class CmHandleRegistrationResponseSpec extends Specification {
 
-    def 'Successful CmHandle Registration Response'() {
-        when: 'CMHandle response is created'
+    def 'Successful cm-handle Registration Response'() {
+        when: 'cm-handle response is created'
             def cmHandleRegistrationResponse = CmHandleRegistrationResponse.createSuccessResponse('cmHandle')
         then: 'a success response is returned'
             with(cmHandleRegistrationResponse) {
@@ -39,8 +39,8 @@ class CmHandleRegistrationResponseSpec extends Specification {
             cmHandleRegistrationResponse.errorText == null
     }
 
-    def 'Failed Cm Handle Registration Response: for unexpected exception'() {
-        when: 'CMHandle response is created for an unexpected exception'
+    def 'Failed cm-handle Registration Response: for unexpected exception'() {
+        when: 'cm-handle response is created for an unexpected exception'
             def cmHandleRegistrationResponse =
                 CmHandleRegistrationResponse.createFailureResponse('cmHandle', new Exception('unexpected error'))
         then: 'the response is created with expected value'
@@ -51,18 +51,21 @@ class CmHandleRegistrationResponseSpec extends Specification {
             }
     }
 
-    def 'Failed Cm Handle Registration Response: for known error'() {
-        when: 'CMHandle response is created for known error'
+    def 'Failed cm-handle Registration Response: for #scenario'() {
+        when: 'cm-handle failure response is created for #scenario'
             def cmHandleRegistrationResponse =
-                CmHandleRegistrationResponse.createFailureResponse('cmHandle', RegistrationError.CM_HANDLE_ALREADY_EXIST)
+                CmHandleRegistrationResponse.createFailureResponse(cmHandleId, registrationError)
         then: 'the response is created with expected value'
             with(cmHandleRegistrationResponse) {
-                assert it.registrationError == RegistrationError.CM_HANDLE_ALREADY_EXIST
-                assert it.cmHandle == 'cmHandle'
+                assert it.registrationError == registrationError
+                assert it.cmHandle == cmHandleId
                 assert it.status == Status.FAILURE
-                assert errorText == RegistrationError.CM_HANDLE_ALREADY_EXIST.errorText
+                assert errorText == registrationError.errorText
             }
-
+        where:
+            scenario                   | cmHandleId  | registrationError
+            'cm-handle already exists' | 'cmHandle'  | RegistrationError.CM_HANDLE_ALREADY_EXIST
+            'cm-handle id is invalid'  | 'cm handle' | RegistrationError.CM_HANDLE_INVALID_ID
     }
 
 }
index a25f81e..5852c0c 100644 (file)
@@ -29,6 +29,8 @@ dataspaces:
     responses:
       '201':
         $ref: 'components.yml#/components/responses/Created'
+      '400':
+        $ref: 'components.yml#/components/responses/BadRequest'
       '401':
         $ref: 'components.yml#/components/responses/Unauthorized'
       '403':
index 58a5ebf..41ad9ca 100755 (executable)
 
 package org.onap.cps.rest.controller
 
-import org.mapstruct.factory.Mappers
-
 import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
 
+import org.mapstruct.factory.Mappers
 import org.onap.cps.api.CpsAdminService
 import org.onap.cps.api.CpsModuleService
 import org.onap.cps.spi.exceptions.AlreadyDefinedException
@@ -73,7 +72,7 @@ class AdminRestControllerSpec extends Specification {
 
     def 'Create new dataspace.'() {
         given: 'an endpoint'
-            def createDataspaceEndpoint = "$basePath/v1/dataspaces";
+            def createDataspaceEndpoint = "$basePath/v1/dataspaces"
         when: 'post is invoked'
             def response =
                     mvc.perform(
@@ -88,7 +87,7 @@ class AdminRestControllerSpec extends Specification {
 
     def 'Create dataspace over existing with same name.'() {
         given: 'an endpoint'
-            def createDataspaceEndpoint = "$basePath/v1/dataspaces";
+            def createDataspaceEndpoint = "$basePath/v1/dataspaces"
         and: 'the service method throws an exception indicating the dataspace is already defined'
             def thrownException = new AlreadyDefinedException(dataspaceName, new RuntimeException())
             mockCpsAdminService.createDataspace(dataspaceName) >> { throw thrownException }
index 44f7f77..2cb01ac 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020 Nordix Foundation
+ *  Copyright (C) 2020-2021 Nordix Foundation
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  ================================================================================
index ecc9bf0..79d6e03 100644 (file)
@@ -23,7 +23,6 @@ package org.onap.cps.api;
 
 import java.util.Collection;
 import java.util.Map;
-import org.checkerframework.checker.nullness.qual.NonNull;
 import org.onap.cps.spi.CascadeDeleteAllowed;
 import org.onap.cps.spi.exceptions.DataInUseException;
 import org.onap.cps.spi.model.ModuleReference;
@@ -42,8 +41,8 @@ public interface CpsModuleService {
      * @param yangResourcesNameToContentMap yang resources (files) as a mep where key is resource name
      *                                      and value is content
      */
-    void createSchemaSet(@NonNull String dataspaceName, @NonNull String schemaSetName,
-                         @NonNull Map<String, String> yangResourcesNameToContentMap);
+    void createSchemaSet(String dataspaceName, String schemaSetName,
+                         Map<String, String> yangResourcesNameToContentMap);
 
     /**
      * Create a schema set from new modules and existing modules.
@@ -52,8 +51,8 @@ public interface CpsModuleService {
      * @param newModuleNameToContentMap YANG resources map where key is a module name and value is content
      * @param moduleReferences          List of YANG resources module references of the modules
      */
-    void createSchemaSetFromModules(@NonNull String dataspaceName, @NonNull String schemaSetName,
-                                    @NonNull Map<String, String> newModuleNameToContentMap,
+    void createSchemaSetFromModules(String dataspaceName, String schemaSetName,
+                                    Map<String, String> newModuleNameToContentMap,
                                     Collection<ModuleReference> moduleReferences);
 
     /**
@@ -63,7 +62,7 @@ public interface CpsModuleService {
      * @param schemaSetName schema set name
      * @return a SchemaSet
      */
-    SchemaSet getSchemaSet(@NonNull String dataspaceName, @NonNull String schemaSetName);
+    SchemaSet getSchemaSet(String dataspaceName, String schemaSetName);
 
     /**
      * Deletes Schema Set.
@@ -74,8 +73,8 @@ public interface CpsModuleService {
      * @throws DataInUseException if cascadeDeleteAllowed is set to CASCADE_DELETE_PROHIBITED and there
      *                           is associated anchor record exists in database
      */
-    void deleteSchemaSet(@NonNull String dataspaceName, @NonNull String schemaSetName,
-        @NonNull CascadeDeleteAllowed cascadeDeleteAllowed);
+    void deleteSchemaSet(String dataspaceName, String schemaSetName,
+                         CascadeDeleteAllowed cascadeDeleteAllowed);
 
     /**
      * Retrieve module references for the given dataspace name.
index beb0a15..68ae1eb 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020 Nordix Foundation
+ *  Copyright (C) 2020-2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -21,7 +21,6 @@
 package org.onap.cps.api;
 
 import java.util.Collection;
-import org.checkerframework.checker.nullness.qual.NonNull;
 import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.model.DataNode;
 
@@ -40,7 +39,7 @@ public interface CpsQueryService {
      *                               included in the output
      * @return a collection of data nodes
      */
-    Collection<DataNode> queryDataNodes(@NonNull String dataspaceName, @NonNull String anchorName,
-        @NonNull String cpsPath, @NonNull FetchDescendantsOption fetchDescendantsOption);
+    Collection<DataNode> queryDataNodes(String dataspaceName, String anchorName,
+                                        String cpsPath, FetchDescendantsOption fetchDescendantsOption);
 
 }
index 1013add..7bec1e3 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020 Nordix Foundation
+ *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  ================================================================================
@@ -30,6 +30,7 @@ import org.onap.cps.api.CpsAdminService;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.spi.CpsAdminPersistenceService;
 import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.utils.CpsValidator;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
@@ -43,42 +44,50 @@ public class CpsAdminServiceImpl implements CpsAdminService {
 
     @Override
     public void createDataspace(final String dataspaceName) {
+        CpsValidator.validateNameCharacters(dataspaceName);
         cpsAdminPersistenceService.createDataspace(dataspaceName);
     }
 
     @Override
     public void deleteDataspace(final String dataspaceName) {
+        CpsValidator.validateNameCharacters(dataspaceName);
         cpsAdminPersistenceService.deleteDataspace(dataspaceName);
     }
 
     @Override
     public void createAnchor(final String dataspaceName, final String schemaSetName, final String anchorName) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName, anchorName);
         cpsAdminPersistenceService.createAnchor(dataspaceName, schemaSetName, anchorName);
     }
 
     @Override
     public Collection<Anchor> getAnchors(final String dataspaceName) {
+        CpsValidator.validateNameCharacters(dataspaceName);
         return cpsAdminPersistenceService.getAnchors(dataspaceName);
     }
 
     @Override
     public Collection<Anchor> getAnchors(final String dataspaceName, final String schemaSetName) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         return cpsAdminPersistenceService.getAnchors(dataspaceName, schemaSetName);
     }
 
     @Override
     public Anchor getAnchor(final String dataspaceName, final String anchorName) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         return cpsAdminPersistenceService.getAnchor(dataspaceName, anchorName);
     }
 
     @Override
     public void deleteAnchor(final String dataspaceName, final String anchorName) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         cpsDataService.deleteDataNodes(dataspaceName, anchorName, OffsetDateTime.now());
         cpsAdminPersistenceService.deleteAnchor(dataspaceName, anchorName);
     }
 
     @Override
     public Collection<String> queryAnchorNames(final String dataspaceName, final Collection<String> moduleNames) {
+        CpsValidator.validateNameCharacters(dataspaceName);
         final Collection<Anchor> anchors = cpsAdminPersistenceService.queryAnchors(dataspaceName, moduleNames);
         return anchors.stream().map(Anchor::getName).collect(Collectors.toList());
     }
index 643614f..399457d 100755 (executable)
@@ -36,6 +36,7 @@ import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.spi.model.Anchor;
 import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.model.DataNodeBuilder;
+import org.onap.cps.utils.CpsValidator;
 import org.onap.cps.utils.YangUtils;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
@@ -56,6 +57,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void saveData(final String dataspaceName, final String anchorName, final String jsonData,
         final OffsetDateTime observedTimestamp) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final var dataNode = buildDataNode(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData);
         cpsDataPersistenceService.storeDataNode(dataspaceName, anchorName, dataNode);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, ROOT_NODE_XPATH, Operation.CREATE);
@@ -64,6 +66,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final String jsonData, final OffsetDateTime observedTimestamp) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
         cpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, parentNodeXpath, dataNode);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.CREATE);
@@ -72,6 +75,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void saveListElements(final String dataspaceName, final String anchorName,
         final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Collection<DataNode> listElementDataNodeCollection =
             buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
         cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
@@ -82,12 +86,14 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
         final FetchDescendantsOption fetchDescendantsOption) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         return cpsDataPersistenceService.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption);
     }
 
     @Override
     public void updateNodeLeaves(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final String jsonData, final OffsetDateTime observedTimestamp) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
         cpsDataPersistenceService
             .updateDataLeaves(dataspaceName, anchorName, dataNode.getXpath(), dataNode.getLeaves());
@@ -102,6 +108,7 @@ public class CpsDataServiceImpl implements CpsDataService {
         final Collection<DataNode> dataNodeUpdates =
             buildDataNodes(dataspaceName, anchorName,
                 parentNodeXpath, dataNodeUpdatesAsJson);
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         for (final DataNode dataNodeUpdate : dataNodeUpdates) {
             processDataNodeUpdate(dataspaceName, anchorName, dataNodeUpdate);
         }
@@ -122,6 +129,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void replaceNodeTree(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final String jsonData, final OffsetDateTime observedTimestamp) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
         cpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName, dataNode);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE);
@@ -130,6 +138,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
             final String jsonData, final OffsetDateTime observedTimestamp) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Collection<DataNode> newListElements =
                 buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
         replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp);
@@ -138,6 +147,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
             final Collection<DataNode> dataNodes, final OffsetDateTime observedTimestamp) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         cpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, dataNodes);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE);
     }
@@ -145,6 +155,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void deleteDataNode(final String dataspaceName, final String anchorName, final String dataNodeXpath,
                                final OffsetDateTime observedTimestamp) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         cpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, dataNodeXpath, Operation.DELETE);
     }
@@ -152,6 +163,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void deleteDataNodes(final String dataspaceName, final String anchorName,
         final OffsetDateTime observedTimestamp) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
         cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName);
         processDataUpdatedEventAsync(anchor, ROOT_NODE_XPATH, Operation.DELETE, observedTimestamp);
@@ -160,6 +172,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void deleteListOrListElement(final String dataspaceName, final String anchorName, final String listNodeXpath,
         final OffsetDateTime observedTimestamp) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         cpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, listNodeXpath);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, listNodeXpath, Operation.DELETE);
     }
index f0e79c6..8e43227 100644 (file)
@@ -33,6 +33,7 @@ import org.onap.cps.spi.exceptions.SchemaSetInUseException;
 import org.onap.cps.spi.model.Anchor;
 import org.onap.cps.spi.model.ModuleReference;
 import org.onap.cps.spi.model.SchemaSet;
+import org.onap.cps.utils.CpsValidator;
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -48,6 +49,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
     @Override
     public void createSchemaSet(final String dataspaceName, final String schemaSetName,
         final Map<String, String> yangResourcesNameToContentMap) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         final var yangTextSchemaSourceSet
             = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap);
         cpsModulePersistenceService.storeSchemaSet(dataspaceName, schemaSetName, yangResourcesNameToContentMap);
@@ -58,6 +60,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
     public void createSchemaSetFromModules(final String dataspaceName, final String schemaSetName,
         final Map<String, String> newModuleNameToContentMap,
         final Collection<ModuleReference> moduleReferences) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         cpsModulePersistenceService.storeSchemaSetFromModules(dataspaceName, schemaSetName,
             newModuleNameToContentMap, moduleReferences);
 
@@ -65,6 +68,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
 
     @Override
     public SchemaSet getSchemaSet(final String dataspaceName, final String schemaSetName) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         final var yangTextSchemaSourceSet = yangTextSchemaSourceSetCache
             .get(dataspaceName, schemaSetName);
         return SchemaSet.builder().name(schemaSetName).dataspaceName(dataspaceName)
@@ -75,6 +79,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
     @Transactional
     public void deleteSchemaSet(final String dataspaceName, final String schemaSetName,
         final CascadeDeleteAllowed cascadeDeleteAllowed) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         final Collection<Anchor> anchors = cpsAdminService.getAnchors(dataspaceName, schemaSetName);
         if (!anchors.isEmpty() && isCascadeDeleteProhibited(cascadeDeleteAllowed)) {
             throw new SchemaSetInUseException(dataspaceName, schemaSetName);
@@ -89,12 +94,14 @@ public class CpsModuleServiceImpl implements CpsModuleService {
 
     @Override
     public Collection<ModuleReference> getYangResourceModuleReferences(final String dataspaceName) {
+        CpsValidator.validateNameCharacters(dataspaceName);
         return cpsModulePersistenceService.getYangResourceModuleReferences(dataspaceName);
     }
 
     @Override
     public Collection<ModuleReference> getYangResourcesModuleReferences(final String dataspaceName,
         final String anchorName) {
+        CpsValidator.validateNameCharacters(dataspaceName);
         return cpsModulePersistenceService.getYangResourceModuleReferences(dataspaceName, anchorName);
     }
 
index dd9f160..c2003d6 100644 (file)
@@ -25,6 +25,7 @@ import org.onap.cps.api.CpsQueryService;
 import org.onap.cps.spi.CpsDataPersistenceService;
 import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.utils.CpsValidator;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -37,6 +38,7 @@ public class CpsQueryServiceImpl implements CpsQueryService {
     @Override
     public Collection<DataNode> queryDataNodes(final String dataspaceName, final String anchorName,
         final String cpsPath, final FetchDescendantsOption fetchDescendantsOption) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         return cpsDataPersistenceService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption);
     }
 }
index 03b52a3..fb881a9 100644 (file)
@@ -24,6 +24,7 @@ package org.onap.cps.api.impl;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.Map;
 import org.onap.cps.spi.CpsModulePersistenceService;
+import org.onap.cps.utils.CpsValidator;
 import org.onap.cps.yang.YangTextSchemaSourceSet;
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -52,6 +53,7 @@ public class YangTextSchemaSourceSetCache {
      */
     @Cacheable(key = "#p0.concat('-').concat(#p1)")
     public YangTextSchemaSourceSet get(final String dataspaceName, final String schemaSetName) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         final Map<String, String> yangResourceNameToContent =
                 cpsModulePersistenceService.getYangSchemaResources(dataspaceName, schemaSetName);
         return YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent);
@@ -69,6 +71,7 @@ public class YangTextSchemaSourceSetCache {
     @CanIgnoreReturnValue
     public YangTextSchemaSourceSet updateCache(final String dataspaceName, final String schemaSetName,
             final YangTextSchemaSourceSet yangTextSchemaSourceSet) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         return yangTextSchemaSourceSet;
     }
 
@@ -81,6 +84,7 @@ public class YangTextSchemaSourceSetCache {
      */
     @CacheEvict(key = "#p0.concat('-').concat(#p1)")
     public void removeFromCache(final String dataspaceName, final String schemaSetName) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         // Spring provides implementation for removing object from cache
     }
 
diff --git a/cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java b/cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java
new file mode 100644 (file)
index 0000000..dd16495
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.utils;
+
+import com.google.common.collect.Lists;
+import java.util.Collection;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.spi.exceptions.DataValidationException;
+
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class CpsValidator {
+
+    private static final char[] UNSUPPORTED_NAME_CHARACTERS = "!\" #$%&'()*+,./\\:;<=>?@[]^`{|}~".toCharArray();
+
+    /**
+     * Validate characters in names within cps.
+     * @param names names of data to be validated
+     */
+    public static void validateNameCharacters(final String... names) {
+        for (final String name : names) {
+            final  Collection<Character> charactersOfName = Lists.charactersOf(name);
+            for (final char unsupportedCharacter : UNSUPPORTED_NAME_CHARACTERS) {
+                if (charactersOfName.contains(unsupportedCharacter)) {
+                    throw new DataValidationException("Name or ID Validation Error.",
+                        name + " invalid token encountered at position " + (name.indexOf(unsupportedCharacter) + 1));
+                }
+            }
+        }
+    }
+}
index eb06199..fc1293c 100644 (file)
@@ -50,9 +50,9 @@ class CpsDataServiceImplSpec extends Specification {
         mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
     }
 
-    def dataspaceName = 'some dataspace'
-    def anchorName = 'some anchor'
-    def schemaSetName = 'some schema set'
+    def dataspaceName = 'some-dataspace'
+    def anchorName = 'some-anchor'
+    def schemaSetName = 'some-schema-set'
     def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build()
     def observedTimestamp = OffsetDateTime.now()
 
index 4878f4c..aa01b44 100644 (file)
@@ -35,8 +35,8 @@ class CpsQueryServiceImplSpec extends Specification {
 
     def 'Query data nodes by cps path with #fetchDescendantsOption.'() {
         given: 'a dataspace name, an anchor name and a cps path'
-            def dataspaceName = 'some dataspace'
-            def anchorName = 'some anchor'
+            def dataspaceName = 'some-dataspace'
+            def anchorName = 'some-anchor'
             def cpsPath = '/cps-path'
         when: 'queryDataNodes is invoked'
             objectUnderTest.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption)
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy
new file mode 100644 (file)
index 0000000..191472c
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.utils
+
+import org.onap.cps.spi.exceptions.DataValidationException
+import spock.lang.Specification
+
+class CpsValidatorSpec extends Specification {
+
+
+    def 'Validating a valid string.'() {
+        when: 'the string is validated using a valid name'
+            CpsValidator.validateNameCharacters('name-with-no-spaces')
+        then: 'no exception is thrown'
+            noExceptionThrown()
+    }
+
+    def 'Validating an invalid string.'() {
+        when: 'the string is validated using an invalid name'
+            CpsValidator.validateNameCharacters(name)
+        then: 'a data validation exception is thrown'
+            def exceptionThrown = thrown(DataValidationException)
+        and: 'the error was encountered at the following index in #scenario'
+            assert exceptionThrown.getDetails().contains(expectedErrorMessage)
+        where: 'the following names are used'
+            scenario     | name               || expectedErrorMessage
+            'position 5' | 'name with spaces' || 'name with spaces invalid token encountered at position 5'
+            'position 9' | 'nameWith Space'   || 'nameWith Space invalid token encountered at position 9'
+    }
+}
index dfad948..7de1f3a 100644 (file)
@@ -42,7 +42,7 @@ Register data node and sync modules.
     ${uri}=              Set Variable       ${ncmpInventoryBasePath}/v1/ch
     ${headers}=          Create Dictionary  Content-Type=application/json   Authorization=${auth}
     ${response}=         POST On Session    CPS_URL   ${uri}   headers=${headers}   data=${jsonDataCreate}
-    Should Be Equal As Strings              ${response.status_code}   204
+    Should Be Equal As Strings              ${response.status_code}   200
 
 Get CM Handle details and confirm it has been registered.
     ${uri}=              Set Variable       ${ncmpBasePath}/v1/ch/PNFDemo
@@ -61,7 +61,7 @@ Update data node and sync modules.
     ${uri}=              Set Variable       ${ncmpInventoryBasePath}/v1/ch
     ${headers}=          Create Dictionary  Content-Type=application/json   Authorization=${auth}
     ${response}=         POST On Session    CPS_URL   ${uri}   headers=${headers}   data=${jsonDataUpdate}
-    Should Be Equal As Strings              ${response.status_code}   204
+    Should Be Equal As Strings              ${response.status_code}   200
 
 Get CM Handle details and confirm it has been updated.
     ${uri}=              Set Variable       ${ncmpBasePath}/v1/ch/PNFDemo
index bc46681..e8a75d9 100644 (file)
@@ -1,6 +1,6 @@
 .. This work is licensed under a Creative Commons Attribution 4.0 International License.
 .. http://creativecommons.org/licenses/by/4.0
-.. Copyright (C) 2021 Nordix Foundation
+.. Copyright (C) 2021-2022 Nordix Foundation
 
 .. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
 .. _design:
@@ -20,17 +20,137 @@ The CPS path parameter is used for querying xpaths. CPS path is inspired by the
 
 This section describes the functionality currently supported by CPS Path.
 
-Sample Data
-===========
+Sample Yang Model
+=================
 
-The xml below describes some basic data to be used to illustrate the CPS Path functionality.
+.. code-block::
+
+  module stores {
+      yang-version 1.1;
+      namespace "org:onap:ccsdk:sample";
+
+      prefix book-store;
+
+      revision "2020-09-15" {
+          description
+            "Sample Model";
+      }
+      container shops {
+
+          container bookstore {
+
+              leaf bookstore-name {
+                  type string;
+              }
+
+              leaf name {
+                  type string;
+              }
+
+              list categories {
+
+                  key "code";
+
+                  leaf code {
+                      type uint16;
+                  }
+
+                  leaf name {
+                      type string;
+                  }
+
+                  leaf numberOfBooks {
+                      type uint16;
+                  }
+
+                  container books {
+
+                      list book {
+                          key title;
+
+                          leaf title {
+                              type string;
+                          }
+                          leaf price {
+                              type uint16;
+                          }
+                          leaf-list label {
+                              type string;
+                          }
+                          leaf-list edition {
+                              type string;
+                          }
+                      }
+                  }
+              }
+          }
+      }
+  }
+
+**Note.** 'categories' is a Yang List and 'code' is its key leaf. All other data nodes are Yang Containers. 'label' and 'edition' are both leaf-lists.
+
+**Note.** CPS accepts only json data. The xml data presented here is for illustration purposes only.
+
+The json and xml below describes some basic data to be used to illustrate the CPS Path functionality.
+
+Sample Data in Json
+===================
+
+.. code-block:: json
+
+    {
+      "shops": {
+        "bookstore": {
+          "bookstore-name": "Chapters",
+          "name": "Chapters",
+          "categories": [
+            {
+              "code": 1,
+              "name": "SciFi",
+              "numberOfBooks": 2,
+              "books": {
+                "book": [
+                  {
+                    "title": "2001: A Space Odyssey",
+                    "price": 5,
+                    "label": ["sale", "classic"],
+                    "edition": ["1968", "2018"]
+                  },
+                  {
+                    "title": "Dune",
+                    "price": 5,
+                    "label": ["classic"],
+                    "edition": ["1965"]
+                  }
+                ]
+              }
+            },
+            {
+              "code": 2,
+              "name": "Kids",
+              "numberOfBooks": 1,
+              "books": {
+                "book": [
+                  {
+                    "title": "Matilda"
+                  }
+                ]
+              }
+            }
+          ]
+        }
+      }
+    }
+
+Sample Data in XML
+==================
 
 .. code-block:: xml
 
     <shops>
        <bookstore name="Chapters">
           <bookstore-name>Chapters</bookstore-name>
-          <categories code="1" name="SciFi" numberOfBooks="2">
+          <categories code=1 name="SciFi" numberOfBooks="2">
              <books>
                 <book title="2001: A Space Odyssey" price="5">
                    <label>sale</label>
@@ -44,7 +164,7 @@ The xml below describes some basic data to be used to illustrate the CPS Path fu
                 </book>
              </books>
           </categories>
-          <categories code="2" name="Kids" numberOfBooks="1">
+          <categories code=2 name="Kids" numberOfBooks="1">
              <books>
                 <book title="Matilda" />
              </books>
@@ -52,8 +172,6 @@ The xml below describes some basic data to be used to illustrate the CPS Path fu
        </bookstore>
     </shops>
 
-**Note.** 'categories' is a Yang List and 'code' is its key leaf. All other data nodes are Yang Containers. 'label' and 'edition' are both leaf-lists.
-
 General Notes
 =============
 
@@ -79,12 +197,14 @@ absolute-path
 
 **Examples**
   - ``/shops/bookstore``
-  - ``/shops/bookstore/categories[@code=1]``
-  - ``/shops/bookstore/categories[@code=1]/book``
+  - ``/shops/bookstore/categories[@code='1']/books``
+  - ``/shops/bookstore/categories[@code='1']/books/book[@title='2001: A Space Odyssey']``
 
 **Limitations**
   - Absolute paths must start with the top element (data node) as per the model tree.
   - Each list reference must include a valid instance reference to the key for that list. Except when it is the last element.
+  - The Absolute path to list with integer key will not work. It needs to be surrounded with a single quote ([@code='1'])
+    as if it is a string. This will be fixed in `CPS-961 <https://jira.onap.org/browse/CPS-961>`_
 
 descendant-path
 ---------------
@@ -95,7 +215,7 @@ descendant-path
 
 **Examples**
   - ``//bookstore``
-  - ``//categories[@code=1]/book``
+  - ``//categories[@code='1']/books``
   - ``//bookstore/categories``
 
 **Limitations**
@@ -113,7 +233,7 @@ leaf-conditions
   - ``/shops/bookstore/categories[@numberOfBooks=1]``
   - ``//categories[@name="Kids"]``
   - ``//categories[@name='Kids']``
-  - ``//categories[@code=1]/books/book[@title='Dune' and @price=5]``
+  - ``//categories[@code='1']/books/book[@title='Dune' and @price=5]``
 
 **Limitations**
   - Only the last list or container can be queried leaf values. Any ancestor list will have to be referenced by its key name-value pair(s).
@@ -156,9 +276,9 @@ The ancestor axis can be added to any CPS path query but has to be the last part
 
 **Examples**
   - ``//book/ancestor::categories``
-  - ``//categories[@genre="SciFi"]/book/ancestor::bookstore``
-  - ``book/ancestor::categories[@code=1]/books``
-  - ``//book/label[text()="classic"]/ancestor::shop``
+  - ``//categories[@code='2']/books/ancestor::bookstore``
+  - ``//book/ancestor::categories[@code='1']/books``
+  - ``//book/label[text()="classic"]/ancestor::shops``
 
 **Limitations**
   - Ancestor list elements can only be addressed using the list key leaf.
index c2e2a5f..2fea4a2 100755 (executable)
@@ -73,6 +73,9 @@ Null can no longer be passed within the dmi plugin service names when registerin
 `CPS-837 <https://jira.onap.org/browse/CPS-837>`_ null is now used to indicate if a property should be removed as part
 of cm handle registration.
 
+The Absolute path to list with integer key will not work. Please refer `CPS-961 <https://jira.onap.org/browse/CPS-961>`_
+for more information.
+
 *Known Vulnerabilities*
 
 None