diff options
40 files changed, 1565 insertions, 301 deletions
diff --git a/checkstyle/pom.xml b/checkstyle/pom.xml index 4e042e97de..bd343683b2 100644 --- a/checkstyle/pom.xml +++ b/checkstyle/pom.xml @@ -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. @@ -27,6 +28,31 @@ <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> @@ -44,6 +70,32 @@ </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 index 0000000000..8f1dbff571 --- /dev/null +++ b/checkstyle/src/main/CopyrightCheck.py @@ -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 index 0000000000..205e0caac2 --- /dev/null +++ b/checkstyle/src/main/resources/copyright-template.txt @@ -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 index 0000000000..4f7394fbb5 --- /dev/null +++ b/checkstyle/src/main/resources/ignore-files-config.csv @@ -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 index 0000000000..85ee43bdab --- /dev/null +++ b/checkstyle/src/main/resources/project-committers-config.csv @@ -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 index 0000000000..177f9d4a51 --- /dev/null +++ b/checkstyle/src/main/test_CopyrightCheck.py @@ -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() diff --git a/cps-ncmp-rest/docs/openapi/components.yaml b/cps-ncmp-rest/docs/openapi/components.yaml index 092c0a28bb..7719193547 100644 --- a/cps-ncmp-rest/docs/openapi/components.yaml +++ b/cps-ncmp-rest/docs/openapi/components.yaml @@ -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: diff --git a/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml b/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml index 3cd8e8baf2..5e61d09625 100755 --- a/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml +++ b/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml @@ -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' diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java index c9d26f2a54..e9a69b3054 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java @@ -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"); @@ -21,10 +21,17 @@ 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()); } } diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy index 9b1c2e87c0..30b6beb379 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy @@ -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) + } + } diff --git a/cps-ncmp-rest/src/test/resources/dmi_registration_all_singing_and_dancing.json b/cps-ncmp-rest/src/test/resources/dmi_registration_all_singing_and_dancing.json index fd8b56b02d..c2a307db26 100644 --- a/cps-ncmp-rest/src/test/resources/dmi_registration_all_singing_and_dancing.json +++ b/cps-ncmp-rest/src/test/resources/dmi_registration_all_singing_and_dancing.json @@ -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" }, diff --git a/cps-ncmp-rest/src/test/resources/dmi_registration_updates_only.json b/cps-ncmp-rest/src/test/resources/dmi_registration_updates_only.json index 58a1a9836b..26acdbdcbe 100644 --- a/cps-ncmp-rest/src/test/resources/dmi_registration_updates_only.json +++ b/cps-ncmp-rest/src/test/resources/dmi_registration_updates_only.json @@ -2,7 +2,7 @@ "dmiPlugin": "service1", "updatedCmHandles":[ { - "cmHandle":"ch3(upd)", + "cmHandle":"ch3-upd", "cmHandleProperties":{ "dmiProp1":"ch3-dmi1", "dmiProp2":null diff --git a/cps-ncmp-rest/src/test/resources/dmi_registration_without_properties.json b/cps-ncmp-rest/src/test/resources/dmi_registration_without_properties.json index 395c098d21..a5dd7b0aad 100644 --- a/cps-ncmp-rest/src/test/resources/dmi_registration_without_properties.json +++ b/cps-ncmp-rest/src/test/resources/dmi_registration_without_properties.json @@ -4,7 +4,7 @@ "dmiModelPlugin":"service3", "createdCmHandles":[ { - "cmHandle": "ch1(new)" + "cmHandle": "ch1-new" } ] } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java index 69e9c7ba1c..1a69e45d30 100755 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java @@ -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)); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java index c838a752ec..ff79f87245 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java @@ -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)); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandle.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandle.java index 47062b3545..e46b9e3da5 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandle.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandle.java @@ -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 index 261a0181cb..0000000000 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandlesList.java +++ /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); - } -} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java index e183ed114b..1da2aa9430 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java @@ -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; diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java index ce2f3e66aa..8a3d26414a 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java @@ -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 diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy index 23d24384c3..cb4d5ef406 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy @@ -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)) + } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy index 06b2032b96..bf5bb73a94 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy @@ -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.'() { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy index f6264f4921..7aacbda513 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy @@ -21,7 +21,10 @@ 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'() { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy index c5ef2f446d..4476998d82 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy @@ -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 } } diff --git a/cps-rest/docs/openapi/cpsAdmin.yml b/cps-rest/docs/openapi/cpsAdmin.yml index a25f81eafc..5852c0cf16 100644 --- a/cps-rest/docs/openapi/cpsAdmin.yml +++ b/cps-rest/docs/openapi/cpsAdmin.yml @@ -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': diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy index 58a5ebf048..41ad9ca5b2 100755 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy @@ -22,14 +22,13 @@ 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 } diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java b/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java index 44f7f77152..2cb01ac1ef 100755 --- a/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java @@ -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 * ================================================================================ diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java index ecc9bf0986..79d6e03d4a 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java @@ -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. diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java b/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java index beb0a1540e..68ae1ebf0a 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java @@ -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); } diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java index 1013addbe1..7bec1e39f0 100755 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java @@ -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()); } diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java index 643614f4fb..399457dd6d 100755 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java @@ -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); } diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java index f0e79c60c7..8e43227f97 100644 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java @@ -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); } diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java index dd9f160dbf..c2003d6bf7 100644 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java @@ -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); } } diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java b/cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java index 03b52a3088..fb881a97b6 100644 --- a/cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java @@ -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 index 0000000000..dd16495638 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java @@ -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)); + } + } + } + } +} diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy index eb06199d1b..fc1293cb76 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy @@ -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() diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy index 4878f4c11b..aa01b44019 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy @@ -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 index 0000000000..191472ceea --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy @@ -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' + } +} diff --git a/csit/tests/cps-model-sync/cps-model-sync.robot b/csit/tests/cps-model-sync/cps-model-sync.robot index dfad948614..7de1f3a1be 100644 --- a/csit/tests/cps-model-sync/cps-model-sync.robot +++ b/csit/tests/cps-model-sync/cps-model-sync.robot @@ -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 diff --git a/docs/cps-path.rst b/docs/cps-path.rst index bc46681d1c..e8a75d9cf0 100644 --- a/docs/cps-path.rst +++ b/docs/cps-path.rst @@ -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. diff --git a/docs/release-notes.rst b/docs/release-notes.rst index c2e2a5fdcd..2fea4a21f1 100755 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -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 |