diff options
132 files changed, 5756 insertions, 1416 deletions
@@ -67,6 +67,11 @@ committers: company: 'Bell Canada' id: 'renukumari' timezone: 'America/Toronto' + - name: 'Joseph Keenan' + email: 'joseph.keenan@est.tech' + company: 'Ericsson Software Technology' + id: 'JosephKeenan' + timezone: 'Europe/Dublin' repositories: - cps tsc: diff --git a/checkstyle/pom.xml b/checkstyle/pom.xml index 07e6cf9663..8d11742000 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. @@ -25,7 +26,32 @@ <modelVersion>4.0.0</modelVersion> <groupId>org.onap.cps</groupId> <artifactId>checkstyle</artifactId> - <version>3.0.0-SNAPSHOT</version> + <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> @@ -34,6 +60,48 @@ <snapshotNexusPath>/content/repositories/snapshots/</snapshotNexusPath> </properties> + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-deploy-plugin</artifactId> + <version>2.8.2</version> + </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> + <successCodes> + <successCode>0</successCode> + <successCode>1</successCode> + </successCodes> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + <distributionManagement> <repository> <id>ecomp-releases</id> 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-application/pom.xml b/cps-application/pom.xml index 50b06b2e69..193599ff9d 100755 --- a/cps-application/pom.xml +++ b/cps-application/pom.xml @@ -27,7 +27,7 @@ <parent> <groupId>org.onap.cps</groupId> <artifactId>cps-parent</artifactId> - <version>3.0.0-SNAPSHOT</version> + <version>3.1.0-SNAPSHOT</version> <relativePath>../cps-parent/pom.xml</relativePath> </parent> diff --git a/cps-bom/pom.xml b/cps-bom/pom.xml index 3e5f70d774..e46892695e 100644 --- a/cps-bom/pom.xml +++ b/cps-bom/pom.xml @@ -25,7 +25,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>org.onap.cps</groupId> <artifactId>cps-bom</artifactId> - <version>3.0.0-SNAPSHOT</version> + <version>3.1.0-SNAPSHOT</version> <packaging>pom</packaging> <description>This artifact contains dependencyManagement declarations of all published CPS components.</description> @@ -37,6 +37,18 @@ <snapshotNexusPath>/content/repositories/snapshots/</snapshotNexusPath> </properties> + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-deploy-plugin</artifactId> + <version>2.8.2</version> + </plugin> + </plugins> + </pluginManagement> + </build> + <distributionManagement> <repository> <id>ecomp-releases</id> diff --git a/cps-dependencies/pom.xml b/cps-dependencies/pom.xml index f04213d439..dcbc5f70bb 100755 --- a/cps-dependencies/pom.xml +++ b/cps-dependencies/pom.xml @@ -3,6 +3,7 @@ ============LICENSE_START======================================================= Copyright (c) 2021 Linux Foundation. Modifications Copyright (C) 2020-2022 Nordix Foundation + Modifications Copyright (C) 2022 Bell Canada. ================================================================================ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,16 +16,18 @@ 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========================================================= --> <project xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.onap.cps</groupId> <artifactId>cps-dependencies</artifactId> - <version>3.0.0-SNAPSHOT</version> + <version>3.1.0-SNAPSHOT</version> <packaging>pom</packaging> <name>${project.groupId}:${project.artifactId}</name> @@ -48,6 +51,18 @@ <mapstruct.version>1.4.2.Final</mapstruct.version> </properties> + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-deploy-plugin</artifactId> + <version>2.8.2</version> + </plugin> + </plugins> + </pluginManagement> + </build> + <distributionManagement> <repository> <id>ecomp-releases</id> @@ -66,23 +81,18 @@ <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> - <version>2.5.5</version> + <version>2.6.4</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> - <version>2020.0.2</version> + <version>2021.0.1</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-web</artifactId> - <version>5.3.13</version> - </dependency> - <dependency> <groupId>org.opendaylight.yangtools</groupId> <artifactId>yangtools-artifacts</artifactId> <version>6.0.1</version> @@ -181,16 +191,6 @@ <scope>test</scope> </dependency> <dependency> - <groupId>org.apache.logging.log4j</groupId> - <artifactId>log4j-api</artifactId> - <version>2.17.1</version> - </dependency> - <dependency> - <groupId>org.apache.logging.log4j</groupId> - <artifactId>log4j-to-slf4j</artifactId> - <version>2.17.1</version> - </dependency> - <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${mapstruct.version}</version> diff --git a/cps-events/pom.xml b/cps-events/pom.xml index b9b399c950..9bd9588271 100644 --- a/cps-events/pom.xml +++ b/cps-events/pom.xml @@ -24,7 +24,7 @@ <parent> <groupId>org.onap.cps</groupId> <artifactId>cps-parent</artifactId> - <version>3.0.0-SNAPSHOT</version> + <version>3.1.0-SNAPSHOT</version> <relativePath>../cps-parent/pom.xml</relativePath> </parent> diff --git a/cps-ncmp-rest/docs/openapi/components.yaml b/cps-ncmp-rest/docs/openapi/components.yaml index 69225aed2d..7ed2efe52a 100644 --- a/cps-ncmp-rest/docs/openapi/components.yaml +++ b/cps-ncmp-rest/docs/openapi/components.yaml @@ -1,6 +1,7 @@ # ============LICENSE_START======================================================= # Copyright (C) 2021-2022 Nordix Foundation # Modifications Copyright (C) 2021 Pantheon.tech +# Modifications Copyright (C) 2022 Bell Canada # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,7 +31,23 @@ components: type: string details: type: string - + # DMI Server Exception Schema + DmiErrorMessage: + title: DMI Error Message + type: object + properties: + message: + type: string + example: "Bad Gateway Error Message NCMP" + dmi-response: + type: object + properties: + http-code: + type: integer + example: 400 + body: + type: string + example: Bad Request # Request Schemas RestDmiPluginRegistration: type: object @@ -70,6 +87,33 @@ 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' + failedUpdatedCmHandles: + type: array + items: + $ref: '#/components/schemas/CmHandlerRegistrationErrorResponse' + failedRemovedCmHandles: + type: array + items: + $ref: '#/components/schemas/CmHandlerRegistrationErrorResponse' + CmHandlerRegistrationErrorResponse: + type: object + properties: + cmHandle: + type: string + example: my-cm-handle + errorCode: + type: string + example: '00' + errorText: + type: string + example: 'Unknown error. <error-details>' RestInputCmHandle: required: @@ -146,6 +190,16 @@ components: type: string example: my-module-revision + CmHandleQueryRestParameters: + type: object + title: Cm Handle query parameters for executing cm handle search + properties: + publicCmHandleProperties: + type: object + additionalProperties: + type: string + example: Book Type + RestOutputCmHandle: type: object title: CM handle Details @@ -303,14 +357,6 @@ components: sample 3: value: resourceIdentifier: parent=shops,child=bookstore - acceptParamInHeader: - name: Accept - in: header - required: false - description: Accept parameter for response, if accept parameter is null, that means client can accept any format. - schema: - type: string - enum: [ application/json, application/yang-data+json ] optionsParamInQuery: name: options in: query @@ -434,3 +480,14 @@ components: status: 500 message: Internal Server Error details: Internal Server Error occurred + BadGateway: + description: Bad Gateway + content: + application/json: + schema: + $ref: "#/components/schemas/DmiErrorMessage" + example: + message: "Bad Gateway Error Message NCMP" + dmi-response: + http-code: 400 + body: "Bad Request" diff --git a/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml b/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml index 3cd8e8baf2..0a408c2413 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,60 @@ updateDmiRegistration: 403: $ref: 'components.yaml#/components/responses/Forbidden' 500: - $ref: 'components.yaml#/components/responses/InternalServerError' + description: Partial or Complete failure. The error details are provided in the response body and all supported error codes are documented in the example. + content: + application/json: + schema: + $ref: 'components.yaml#/components/schemas/DmiPluginRegistrationErrorResponse' + example: + failedCreatedCmHandles: [ + { + "cmHandle": "my-cm-handle-01", + "errorCode": "00", + "errorText": "Unknown error. <error-details>" + }, + { + "cmHandle": "my-cm-handle-02", + "errorCode": "01", + "errorText": "cm-handle already exists" + }, + { + "cmHandle": "my-cm-handle-03", + "errorCode": "03", + "errorText": "cm-handle has an invalid character(s) in id" + } + ] + failedUpdatedCmHandles: [ + { + "cmHandle": "my-cm-handle-01", + "errorCode": "00", + "errorText": "Unknown error. <error-details>" + }, + { + "cmHandle": "my-cm-handle-02", + "errorCode": "02", + "errorText": "cm-handle does not exist" + }, + { + "cmHandle": "my-cm-handle-03", + "errorCode": "03", + "errorText": "cm-handle has an invalid character(s) in id" + } + ] + failedRemovedCmHandles: [ + { + "cmHandle": "my-cm-handle-01", + "errorCode": "00", + "errorText": "Unknown error. <error-details>" + }, + { + "cmHandle": "my-cm-handle-02", + "errorCode": "02", + "errorText": "cm-handle does not exists" + }, + { + "cmHandle": "my-cm-handle-03", + "errorCode": "03", + "errorText": "cm-handle has an invalid character(s) in id" + } + ] diff --git a/cps-ncmp-rest/docs/openapi/ncmp.yml b/cps-ncmp-rest/docs/openapi/ncmp.yml index a9d08b7951..05e4b84853 100755 --- a/cps-ncmp-rest/docs/openapi/ncmp.yml +++ b/cps-ncmp-rest/docs/openapi/ncmp.yml @@ -1,7 +1,7 @@ # ============LICENSE_START======================================================= # Copyright (C) 2021-2022 Nordix Foundation # Modifications Copyright (C) 2021 Pantheon.tech -# Modifications Copyright (C) 2021 Bell Canada +# Modifications Copyright (C) 2021-2022 Bell Canada # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ getResourceDataForPassthroughOperational: parameters: - $ref: 'components.yaml#/components/parameters/cmHandleInPath' - $ref: 'components.yaml#/components/parameters/resourceIdentifierInQuery' - - $ref: 'components.yaml#/components/parameters/acceptParamInHeader' - $ref: 'components.yaml#/components/parameters/optionsParamInQuery' - $ref: 'components.yaml#/components/parameters/topicParamInQuery' responses: @@ -48,6 +47,8 @@ getResourceDataForPassthroughOperational: $ref: 'components.yaml#/components/responses/Forbidden' 500: $ref: 'components.yaml#/components/responses/InternalServerError' + 502: + $ref: 'components.yaml#/components/responses/BadGateway' resourceDataForPassthroughRunning: get: @@ -59,7 +60,6 @@ resourceDataForPassthroughRunning: parameters: - $ref: 'components.yaml#/components/parameters/cmHandleInPath' - $ref: 'components.yaml#/components/parameters/resourceIdentifierInQuery' - - $ref: 'components.yaml#/components/parameters/acceptParamInHeader' - $ref: 'components.yaml#/components/parameters/optionsParamInQuery' - $ref: 'components.yaml#/components/parameters/topicParamInQuery' responses: @@ -80,6 +80,8 @@ resourceDataForPassthroughRunning: $ref: 'components.yaml#/components/responses/Forbidden' 500: $ref: 'components.yaml#/components/responses/InternalServerError' + 502: + $ref: 'components.yaml#/components/responses/BadGateway' post: tags: - network-cm-proxy @@ -116,6 +118,8 @@ resourceDataForPassthroughRunning: $ref: 'components.yaml#/components/responses/Forbidden' 500: $ref: 'components.yaml#/components/responses/InternalServerError' + 502: + $ref: 'components.yaml#/components/responses/BadGateway' put: tags: @@ -153,6 +157,8 @@ resourceDataForPassthroughRunning: $ref: 'components.yaml#/components/responses/Forbidden' 500: $ref: 'components.yaml#/components/responses/InternalServerError' + 502: + $ref: 'components.yaml#/components/responses/BadGateway' patch: tags: @@ -184,6 +190,8 @@ resourceDataForPassthroughRunning: $ref: 'components.yaml#/components/responses/Forbidden' 500: $ref: 'components.yaml#/components/responses/InternalServerError' + 502: + $ref: 'components.yaml#/components/responses/BadGateway' delete: tags: @@ -208,6 +216,8 @@ resourceDataForPassthroughRunning: $ref: 'components.yaml#/components/responses/NotFound' 500: $ref: 'components.yaml#/components/responses/InternalServerError' + 502: + $ref: 'components.yaml#/components/responses/BadGateway' fetchModuleReferencesByCmHandle: get: @@ -281,6 +291,33 @@ retrieveCmHandleDetailsById: application/json: schema: $ref: 'components.yaml#/components/schemas/RestOutputCmHandle' + 404: + $ref: 'components.yaml#/components/responses/NotFound' + 500: + $ref: 'components.yaml#/components/responses/InternalServerError' + +queryCmHandles: + post: + description: Execute cm handle query search + tags: + - network-cm-proxy + summary: Execute cm handle query upon a given set of query parameters + operationId: queryCmHandles + requestBody: + required: true + content: + application/json: + schema: + $ref: 'components.yaml#/components/schemas/CmHandleQueryRestParameters' + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + type: string 400: $ref: 'components.yaml#/components/responses/BadRequest' 401: diff --git a/cps-ncmp-rest/docs/openapi/openapi.yml b/cps-ncmp-rest/docs/openapi/openapi.yml index 12a8318efb..935b657e1f 100755 --- a/cps-ncmp-rest/docs/openapi/openapi.yml +++ b/cps-ncmp-rest/docs/openapi/openapi.yml @@ -39,4 +39,7 @@ paths: $ref: 'ncmp.yml#/executeCmHandleSearch' /v1/ch/{cm-handle}: - $ref: 'ncmp.yml#/retrieveCmHandleDetailsById'
\ No newline at end of file + $ref: 'ncmp.yml#/retrieveCmHandleDetailsById' + + /v1/data/ch/searches: + $ref: 'ncmp.yml#/queryCmHandles' diff --git a/cps-ncmp-rest/pom.xml b/cps-ncmp-rest/pom.xml index 97305cfe98..6a700c3e12 100644 --- a/cps-ncmp-rest/pom.xml +++ b/cps-ncmp-rest/pom.xml @@ -27,7 +27,7 @@ <parent> <groupId>org.onap.cps</groupId> <artifactId>cps-parent</artifactId> - <version>3.0.0-SNAPSHOT</version> + <version>3.1.0-SNAPSHOT</version> <relativePath>../cps-parent/pom.xml</relativePath> </parent> diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapper.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapper.java index 4c8fafea5f..a9ec863d53 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapper.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapper.java @@ -45,7 +45,7 @@ public interface NcmpRestInputMapper { nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) DmiPluginRegistration toDmiPluginRegistration(final RestDmiPluginRegistration restDmiPluginRegistration); - @Mapping(source = "cmHandle", target = "cmHandleID") + @Mapping(source = "cmHandle", target = "cmHandleId") @Mapping(source = "cmHandleProperties", target = "dmiProperties") @Mapping(source = "publicCmHandleProperties", target = "publicProperties") NcmpServiceCmHandle toNcmpServiceCmHandle(final RestInputCmHandle restInputCmHandle); diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java index 0201fad2b5..5c1f8704da 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java @@ -3,7 +3,7 @@ * Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Nordix Foundation * Modification Copyright (C) 2021 highstreet technologies GmbH - * Modifications (C) 2021 Bell Canada + * Modifications (C) 2021-2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,18 +31,25 @@ import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import javax.validation.Valid; import javax.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.NetworkCmProxyDataService; +import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException; +import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; import org.onap.cps.ncmp.rest.api.NetworkCmProxyApi; import org.onap.cps.ncmp.rest.model.CmHandleProperties; import org.onap.cps.ncmp.rest.model.CmHandleProperty; import org.onap.cps.ncmp.rest.model.CmHandlePublicProperties; +import org.onap.cps.ncmp.rest.model.CmHandleQueryRestParameters; import org.onap.cps.ncmp.rest.model.CmHandles; import org.onap.cps.ncmp.rest.model.ConditionProperties; import org.onap.cps.ncmp.rest.model.Conditions; @@ -50,6 +57,7 @@ import org.onap.cps.ncmp.rest.model.ModuleNameAsJsonObject; import org.onap.cps.ncmp.rest.model.ModuleNamesAsJsonArray; import org.onap.cps.ncmp.rest.model.RestModuleReference; import org.onap.cps.ncmp.rest.model.RestOutputCmHandle; +import org.onap.cps.utils.CpsValidator; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -63,6 +71,9 @@ import org.springframework.web.bind.annotation.RestController; public class NetworkCmProxyController implements NetworkCmProxyApi { private static final String NO_BODY = null; + private static final String NO_REQUEST_ID = null; + private static final String NO_TOPIC = null; + public static final String ASYNC_REQUEST_ID = "requestId"; private final NetworkCmProxyDataService networkCmProxyDataService; private final JsonObjectMapper jsonObjectMapper; @@ -73,7 +84,6 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { * * @param cmHandle cm handle identifier * @param resourceIdentifier resource identifier - * @param acceptParamInHeader accept header parameter * @param optionsParamInQuery options query parameter * @param topicParamInQuery topic query parameter * @return {@code ResponseEntity} response from dmi plugin @@ -81,15 +91,21 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { @Override public ResponseEntity<Object> getResourceDataOperationalForCmHandle(final String cmHandle, final @NotNull @Valid String resourceIdentifier, - final String acceptParamInHeader, final @Valid String optionsParamInQuery, final @Valid String topicParamInQuery) { + final ResponseEntity<Map<String, Object>> asyncResponse = populateAsyncResponse(topicParamInQuery); + final Map<String, Object> asyncResponseData = asyncResponse.getBody(); + final Object responseObject = networkCmProxyDataService.getResourceDataOperationalForCmHandle(cmHandle, resourceIdentifier, - acceptParamInHeader, optionsParamInQuery, - topicParamInQuery); - return ResponseEntity.ok(responseObject); + asyncResponseData == null ? NO_TOPIC : topicParamInQuery, + asyncResponseData == null ? NO_REQUEST_ID : asyncResponseData.get(ASYNC_REQUEST_ID).toString()); + + if (asyncResponseData == null) { + return ResponseEntity.ok(responseObject); + } + return ResponseEntity.ok(asyncResponse); } /** @@ -97,7 +113,6 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { * * @param cmHandle cm handle identifier * @param resourceIdentifier resource identifier - * @param acceptParamInHeader accept header parameter * @param optionsParamInQuery options query parameter * @param topicParamInQuery topic query parameter * @return {@code ResponseEntity} response from dmi plugin @@ -105,15 +120,21 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { @Override public ResponseEntity<Object> getResourceDataRunningForCmHandle(final String cmHandle, final @NotNull @Valid String resourceIdentifier, - final String acceptParamInHeader, final @Valid String optionsParamInQuery, final @Valid String topicParamInQuery) { + final ResponseEntity<Map<String, Object>> asyncResponse = populateAsyncResponse(topicParamInQuery); + final Map<String, Object> asyncResponseData = asyncResponse.getBody(); + final Object responseObject = networkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle(cmHandle, resourceIdentifier, - acceptParamInHeader, optionsParamInQuery, - topicParamInQuery); - return ResponseEntity.ok(responseObject); + asyncResponseData == null ? NO_TOPIC : topicParamInQuery, + asyncResponseData == null ? NO_REQUEST_ID : asyncResponseData.get(ASYNC_REQUEST_ID).toString()); + + if (asyncResponseData == null) { + return ResponseEntity.ok(responseObject); + } + return ResponseEntity.ok(asyncResponse); } @Override @@ -195,6 +216,19 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { } /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryRestParameters the cm handle query parameters + * @return collection of cm handle ids + */ + public ResponseEntity<List<String>> queryCmHandles( + final CmHandleQueryRestParameters cmHandleQueryRestParameters) { + final Set<String> cmHandleIds = networkCmProxyDataService.queryCmHandles( + jsonObjectMapper.convertToValueType(cmHandleQueryRestParameters, CmHandleQueryApiParameters.class)); + return ResponseEntity.ok(List.copyOf(cmHandleIds)); + } + + /** * Search for Cm Handle and Properties by Name. * @param cmHandleId cm-handle identifier * @return cm handle and its properties @@ -258,9 +292,38 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { private RestOutputCmHandle toRestOutputCmHandle(final NcmpServiceCmHandle ncmpServiceCmHandle) { final RestOutputCmHandle restOutputCmHandle = new RestOutputCmHandle(); final CmHandlePublicProperties cmHandlePublicProperties = new CmHandlePublicProperties(); - restOutputCmHandle.setCmHandle(ncmpServiceCmHandle.getCmHandleID()); + restOutputCmHandle.setCmHandle(ncmpServiceCmHandle.getCmHandleId()); cmHandlePublicProperties.add(ncmpServiceCmHandle.getPublicProperties()); restOutputCmHandle.setPublicCmHandleProperties(cmHandlePublicProperties); return restOutputCmHandle; } + + private ResponseEntity<Map<String, Object>> populateAsyncResponse(final String topicParamInQuery) { + final boolean processAsynchronously = hasTopicParameter(topicParamInQuery); + final Map<String, Object> responseData; + if (processAsynchronously) { + responseData = getAsyncResponseData(); + } else { + responseData = null; + } + return ResponseEntity.ok().body(responseData); + } + + private static boolean hasTopicParameter(final String topicName) { + if (topicName == null) { + return false; + } + if (CpsValidator.validateTopicName(topicName)) { + return true; + } + throw new InvalidTopicException("Topic name " + topicName + " is invalid", "invalid topic"); + } + + private Map<String, Object> getAsyncResponseData() { + final Map<String, Object> asyncResponseData = new HashMap<>(1); + final String resourceDataRequestId = UUID.randomUUID().toString(); + asyncResponseData.put(ASYNC_REQUEST_ID, resourceDataRequestId); + return asyncResponseData; + } + } 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..105a6a559c 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,58 @@ 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 DmiPluginRegistrationErrorResponse 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/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java index 0843e9741e..c72373344d 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java @@ -24,11 +24,14 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.impl.exception.DmiRequestException; +import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException; import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException; import org.onap.cps.ncmp.api.impl.exception.NcmpException; import org.onap.cps.ncmp.api.impl.exception.ServerNcmpException; import org.onap.cps.ncmp.rest.controller.NetworkCmProxyController; import org.onap.cps.ncmp.rest.controller.NetworkCmProxyInventoryController; +import org.onap.cps.ncmp.rest.model.DmiErrorMessage; +import org.onap.cps.ncmp.rest.model.DmiErrorMessageDmiresponse; import org.onap.cps.ncmp.rest.model.ErrorMessage; import org.onap.cps.spi.exceptions.CpsException; import org.onap.cps.spi.exceptions.DataNodeNotFoundException; @@ -66,6 +69,12 @@ public class NetworkCmProxyRestExceptionHandler { return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, exception); } + @ExceptionHandler({HttpClientRequestException.class}) + public static ResponseEntity<Object> handleClientRequestExceptions( + final HttpClientRequestException httpClientRequestException) { + return wrapDmiErrorResponse(HttpStatus.BAD_GATEWAY, httpClientRequestException); + } + @ExceptionHandler({DmiRequestException.class, DataValidationException.class, HttpMessageNotReadableException.class, InvalidTopicException.class}) public static ResponseEntity<Object> handleDmiRequestExceptions(final Exception exception) { @@ -91,8 +100,19 @@ public class NetworkCmProxyRestExceptionHandler { } else { errorMessage.setDetails(CHECK_LOGS_FOR_DETAILS); } - errorMessage.setDetails(exception instanceof CpsException ? ((CpsException) exception).getDetails() : - CHECK_LOGS_FOR_DETAILS); + errorMessage.setDetails( + exception instanceof CpsException ? ((CpsException) exception).getDetails() : CHECK_LOGS_FOR_DETAILS); return new ResponseEntity<>(errorMessage, status); } + + private static ResponseEntity<Object> wrapDmiErrorResponse(final HttpStatus httpStatus, + final HttpClientRequestException httpClientRequestException) { + final var dmiErrorMessage = new DmiErrorMessage(); + final var dmiErrorResponse = new DmiErrorMessageDmiresponse(); + dmiErrorResponse.setHttpCode(httpClientRequestException.getHttpStatus()); + dmiErrorResponse.setBody(httpClientRequestException.getDetails()); + dmiErrorMessage.setMessage(httpClientRequestException.getMessage()); + dmiErrorMessage.setDmiResponse(dmiErrorResponse); + return new ResponseEntity<>(dmiErrorMessage, httpStatus); + } } diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapperSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapperSpec.groovy index 3d54a0b089..bb762080d2 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapperSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapperSpec.groovy @@ -43,7 +43,7 @@ class NcmpRestInputMapperSpec extends Specification { then: 'the result returns the correct number of cm handles' result.createdCmHandles.size() == 1 and: 'the converted cm handle has the same id' - result.createdCmHandles[0].cmHandleID == 'example-id' + result.createdCmHandles[0].cmHandleId == 'example-id' and: '(empty) properties are converted correctly' result.createdCmHandles[0].dmiProperties == expectedDmiProperties result.createdCmHandles[0].publicProperties == expectedPublicProperties diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy index d5c3cd9f37..b34b0fff38 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy @@ -3,7 +3,7 @@ * Copyright (C) 2021 Pantheon.tech * Modification Copyright (C) 2021 highstreet technologies GmbH * Modification Copyright (C) 2021-2022 Nordix Foundation - * Modification Copyright (C) 2021 Bell Canada. + * Modification Copyright (C) 2021-2022 Bell Canada. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,7 +60,10 @@ class NetworkCmProxyControllerSpec extends Specification { NetworkCmProxyDataService mockNetworkCmProxyDataService = Mock() @SpringBean - JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + ObjectMapper objectMapper = new ObjectMapper() + + @SpringBean + JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper) @SpringBean NcmpRestInputMapper ncmpRestInputMapper = Mappers.getMapper(NcmpRestInputMapper) @@ -72,6 +75,7 @@ class NetworkCmProxyControllerSpec extends Specification { @Shared def NO_TOPIC = null + def NO_REQUEST_ID = null def 'Get Resource Data from pass-through operational.'() { given: 'resource data url' @@ -81,43 +85,51 @@ class NetworkCmProxyControllerSpec extends Specification { def response = mvc.perform( get(getUrl) .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON_VALUE) ).andReturn().response then: 'the NCMP data service is called with getResourceDataOperationalForCmHandle' 1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle', 'parent/child', - 'application/json', '(a=1,b=2)', - NO_TOPIC) + NO_TOPIC, + NO_REQUEST_ID) and: 'response status is Ok' response.status == HttpStatus.OK.value() } - def 'Get Resource Data from pass-through operational with #scenario.'() { + def 'Get Resource Data from #datastoreInUrl with #scenario.'() { given: 'resource data url' - def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" + + def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" + "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}" when: 'get data resource request is performed' def response = mvc.perform( get(getUrl) .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON_VALUE) ).andReturn().response then: 'the NCMP data service is called with operational data for cm handle' - 1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle', + expectedNumberOfMethodExecutions + * mockNetworkCmProxyDataService."${expectedMethodName}"('testCmHandle', 'parent/child', - 'application/json', '(a=1,b=2)', - expectedTopicName) - and: 'response status is Ok' - response.status == HttpStatus.OK.value() + expectedTopicName, + _) + then: 'response status is expected' + response.status == expectedHttpStatus where: 'the following parameters are used' - scenario | topicQueryParam || expectedTopicName - 'Url with valid topic' | "&topic=my-topic-name" || "my-topic-name" - 'No topic in url' | '' || NO_TOPIC - 'Null topic in url' | "&topic=null" || "null" - 'Empty topic in url' | "&topic=\"\"" || "\"\"" - 'Missing topic in url' | "&topic=" || "" + scenario | datastoreInUrl | topicQueryParam || expectedTopicName | expectedMethodName | expectedNumberOfMethodExecutions | expectedHttpStatus + 'url with valid topic' | 'passthrough-operational' | '&topic=my-topic-name' || 'my-topic-name' | 'getResourceDataOperationalForCmHandle' | 1 | HttpStatus.OK.value() + 'no topic in url' | 'passthrough-operational' | '' || NO_TOPIC | 'getResourceDataOperationalForCmHandle' | 1 | HttpStatus.OK.value() + 'null topic in url' | 'passthrough-operational' | '&topic=null' || 'null' | 'getResourceDataOperationalForCmHandle' | 1 | HttpStatus.OK.value() + 'empty topic in url' | 'passthrough-operational' | '&topic=\"\"' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'missing topic in url' | 'passthrough-operational' | '&topic=' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'blank topic value in url' | 'passthrough-operational' | '&topic=\" \"' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'invalid non-empty topic value in url' | 'passthrough-operational' | '&topic=1_5_*_#' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'url with valid topic' | 'passthrough-running' | '&topic=my-topic-name' || 'my-topic-name' | 'getResourceDataPassThroughRunningForCmHandle' | 1 | HttpStatus.OK.value() + 'no topic in url' | 'passthrough-running' | '' || NO_TOPIC | 'getResourceDataPassThroughRunningForCmHandle' | 1 | HttpStatus.OK.value() + 'null topic in url' | 'passthrough-running' | '&topic=null' || 'null' | 'getResourceDataPassThroughRunningForCmHandle' | 1 | HttpStatus.OK.value() + 'empty topic in url' | 'passthrough-running' | '&topic=\"\"' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'missing topic in url' | 'passthrough-running' | '&topic=' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'blank topic value in url' | 'passthrough-running' | '&topic=\" \"' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'invalid non-empty topic value in url' | 'passthrough-running' | '&topic=1_5_*_#' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() } def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() { @@ -127,14 +139,13 @@ class NetworkCmProxyControllerSpec extends Specification { and: 'ncmp service returns json object' mockNetworkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle('testCmHandle', resourceIdentifier, - 'application/json', '(a=1,b=2)', - NO_TOPIC) >> '{valid-json}' + NO_TOPIC, + NO_REQUEST_ID) >> '{valid-json}' when: 'get data resource request is performed' def response = mvc.perform( get(getUrl) .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON_VALUE) ).andReturn().response then: 'response status is Ok' response.status == HttpStatus.OK.value() @@ -157,8 +168,7 @@ class NetworkCmProxyControllerSpec extends Specification { when: 'update data resource request is performed' def response = mvc.perform( put(updateUrl) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .accept(MediaType.APPLICATION_JSON_VALUE).content(requestBody) + .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody) ).andReturn().response then: 'ncmp service method to update resource is called' 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', @@ -175,8 +185,7 @@ class NetworkCmProxyControllerSpec extends Specification { when: 'create resource request is performed' def response = mvc.perform( post(url) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .accept(MediaType.APPLICATION_JSON_VALUE).content(requestBody) + .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody) ).andReturn().response then: 'ncmp service method to create resource called' 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', @@ -222,7 +231,7 @@ class NetworkCmProxyControllerSpec extends Specification { def cmHandleId = 'Some-Cm-Handle' def dmiProperties = [ prop:'some DMI property' ] def publicProperties = [ "public prop":'some public property' ] - def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleID: cmHandleId, dmiProperties: dmiProperties, publicProperties: publicProperties) + def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: dmiProperties, publicProperties: publicProperties) and: 'the service method is invoked with the cm handle id' 1 * mockNetworkCmProxyDataService.getNcmpServiceCmHandle('Some-Cm-Handle') >> ncmpServiceCmHandle when: 'the cm handle details api is invoked' @@ -249,6 +258,31 @@ class NetworkCmProxyControllerSpec extends Specification { response.contentAsString == '{"cmHandles":[]}' } + def 'Query for cm handles matching query parameters'() { + given: 'an endpoint and json data' + def searchesEndpoint = "$ncmpBasePathV1/data/ch/searches" + String jsonString = '{"publicCmHandleProperties": {"name": "Contact", "value": "newemailforstore@bookstore.com"}}' + and: 'the service method is invoked with module names and returns cm handle ids' + 1 * mockNetworkCmProxyDataService.queryCmHandles(_) >> ['some-cmhandle-id1', 'some-cmhandle-id2'] + when: 'the searches api is invoked' + def response = mvc.perform(post(searchesEndpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString)).andReturn().response + then: 'cm handle ids are returned' + response.contentAsString == '["some-cmhandle-id1","some-cmhandle-id2"]' + } + + def 'Query for cm handles with invalid request payload'() { + when: 'the searches api is invoked' + def searchesEndpoint = "$ncmpBasePathV1/data/ch/searches" + def invalidInputData = '{invalidJson}' + def response = mvc.perform(post(searchesEndpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(invalidInputData)).andReturn().response + then: 'BAD_REQUEST is returned' + response.getStatus() == 400 + } + def 'Patch resource data in pass-through running datastore.' () { given: 'patch resource data url' def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" + @@ -279,5 +313,24 @@ class NetworkCmProxyControllerSpec extends Specification { and: 'the response is No Content' response.status == HttpStatus.NO_CONTENT.value() } + + def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() { + given: 'resource data url' + def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" + + "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name" + when: 'get data resource request is performed' + def response = mvc.perform( + get(getUrl) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON_VALUE) + ).andReturn().response + then: 'async request id is generated' + assert response.contentAsString.contains("requestId") + where: 'the following parameters are used' + scenario | datastoreInUrl + ':passthrough-operational' | 'passthrough-operational' + ':passthrough-running' | 'passthrough-running' + } + } 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/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy index b642370154..1f6c38428b 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy @@ -21,14 +21,13 @@ package org.onap.cps.ncmp.rest.exceptions -import com.fasterxml.jackson.databind.ObjectMapper import groovy.json.JsonSlurper import org.mapstruct.factory.Mappers import org.onap.cps.TestUtils import org.onap.cps.ncmp.api.NetworkCmProxyDataService import org.onap.cps.ncmp.api.impl.exception.DmiRequestException +import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException import org.onap.cps.ncmp.api.impl.exception.ServerNcmpException -import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle import org.onap.cps.ncmp.rest.controller.NcmpRestInputMapper import org.onap.cps.spi.exceptions.CpsException import org.onap.cps.spi.exceptions.DataNodeNotFoundException @@ -38,6 +37,7 @@ import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import spock.lang.Shared @@ -111,6 +111,19 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification { assertTestResponse(response, BAD_REQUEST, sampleErrorMessage, sampleErrorDetails) } + def 'Failing DMI Request - passthrough scenario'() { + given: 'failing DMI request' + mockNetworkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle(*_) >> { throw new HttpClientRequestException('Error Message Details NCMP', 'Bad Request from DMI', 400) } + when: 'the DMI request is executed' + def response = mvc.perform(get("$dataNodeBaseEndpointNcmp/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=stores:bookstore/categories=100")) + .andReturn().response + then: 'NCMP service responds with 502 Bad Gateway status' + response.status == HttpStatus.BAD_GATEWAY.value() + and: 'the NCMP response also contains the original DMI response details' + response.contentAsString.contains('400') + response.contentAsString.contains('Bad Request from DMI') + } + def setupTestException(exception, apiType) { if (NCMP == apiType) { mockNetworkCmProxyDataService.getYangResourcesModuleReferences(*_) >> { throw exception } 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/pom.xml b/cps-ncmp-service/pom.xml index fe061eaed0..573c76e4a8 100644 --- a/cps-ncmp-service/pom.xml +++ b/cps-ncmp-service/pom.xml @@ -26,7 +26,7 @@ <parent> <groupId>org.onap.cps</groupId> <artifactId>cps-parent</artifactId> - <version>3.0.0-SNAPSHOT</version> + <version>3.1.0-SNAPSHOT</version> <relativePath>../cps-parent/pom.xml</relativePath> </parent> diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java index d942d26c88..058c42b7b9 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java @@ -3,6 +3,7 @@ * Copyright (C) 2021 highstreet technologies GmbH * Modifications Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech + * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +26,10 @@ package org.onap.cps.ncmp.api; import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum; import java.util.Collection; +import java.util.Set; +import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters; 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.model.ModuleReference; @@ -38,8 +42,9 @@ public interface NetworkCmProxyDataService { * Registration of New CM Handles. * * @param dmiPluginRegistration Dmi Plugin Registration + * @return dmiPluginRegistrationResponse */ - void updateDmiRegistrationAndSyncModule(DmiPluginRegistration dmiPluginRegistration); + DmiPluginRegistrationResponse updateDmiRegistrationAndSyncModule(DmiPluginRegistration dmiPluginRegistration); /** * Get resource data for data store pass-through operational @@ -47,16 +52,16 @@ public interface NetworkCmProxyDataService { * * @param cmHandleId cm handle identifier * @param resourceIdentifier resource identifier - * @param acceptParamInHeader accept param * @param optionsParamInQuery options query * @param topicParamInQuery topic name for (triggering) async responses + * @param requestId unique requestId for async request * @return {@code Object} resource data */ Object getResourceDataOperationalForCmHandle(String cmHandleId, String resourceIdentifier, - String acceptParamInHeader, String optionsParamInQuery, - String topicParamInQuery); + String topicParamInQuery, + String requestId); /** * Get resource data for data store pass-through running @@ -64,16 +69,16 @@ public interface NetworkCmProxyDataService { * * @param cmHandleId cm handle identifier * @param resourceIdentifier resource identifier - * @param acceptParamInHeader accept param * @param optionsParamInQuery options query - * @param topicParamInQuery topic query + * @param topicParamInQuery topic name for (triggering) async responses + * @param requestId unique requestId for async request * @return {@code Object} resource data */ Object getResourceDataPassThroughRunningForCmHandle(String cmHandleId, String resourceIdentifier, - String acceptParamInHeader, String optionsParamInQuery, - String topicParamInQuery); + String topicParamInQuery, + String requestId); /** * Write resource data for data store pass-through running @@ -116,4 +121,11 @@ public interface NetworkCmProxyDataService { */ NcmpServiceCmHandle getNcmpServiceCmHandle(String cmHandleId); + /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryApiParameters the cm handle query parameters + * @return collection of cm handle ids + */ + Set<String> queryCmHandles(CmHandleQueryApiParameters cmHandleQueryApiParameters); } 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 76d4cef9e8..e624953f54 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 @@ -3,7 +3,7 @@ * Copyright (C) 2021 highstreet technologies GmbH * Modifications Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2021 Bell Canada + * Modifications Copyright (C) 2021-2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,14 +31,13 @@ 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 com.google.common.base.Strings; +import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.UUID; -import java.util.regex.Pattern; +import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -46,21 +45,25 @@ import org.onap.cps.api.CpsAdminService; import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsModuleService; import org.onap.cps.ncmp.api.NetworkCmProxyDataService; -import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException; -import org.onap.cps.ncmp.api.impl.exception.ServerNcmpException; +import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException; import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations; -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.inventory.sync.ModuleSyncService; +import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters; +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; import org.springframework.stereotype.Service; @@ -75,8 +78,6 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService private final DmiDataOperations dmiDataOperations; - private final DmiModelOperations dmiModelOperations; - private final CpsModuleService cpsModuleService; private final CpsAdminService cpsAdminService; @@ -85,50 +86,47 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService private final YangModelCmHandleRetriever yangModelCmHandleRetriever; - // valid kafka topic name regex - private static final Pattern TOPIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9]([._-](?![._-])|" - + "[a-zA-Z0-9]){0,120}[a-zA-Z0-9]$"); - private static final String NO_REQUEST_ID = null; - private static final String NO_TOPIC = null; + private final ModuleSyncService moduleSyncService; @Override - public void updateDmiRegistrationAndSyncModule(final DmiPluginRegistration dmiPluginRegistration) { + public DmiPluginRegistrationResponse updateDmiRegistrationAndSyncModule( + final DmiPluginRegistration dmiPluginRegistration) { dmiPluginRegistration.validateDmiPluginRegistration(); - try { - if (!dmiPluginRegistration.getCreatedCmHandles().isEmpty()) { - parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration); - } - if (!dmiPluginRegistration.getUpdatedCmHandles().isEmpty()) { - parseAndUpdateCmHandlesInDmiRegistration(dmiPluginRegistration); - } - parseAndRemoveCmHandlesInDmiRegistration(dmiPluginRegistration); - } 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); + final var dmiPluginRegistrationResponse = new DmiPluginRegistrationResponse(); + 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; } @Override public Object getResourceDataOperationalForCmHandle(final String cmHandleId, final String resourceIdentifier, - final String acceptParamInHeader, final String optionsParamInQuery, - final String topicParamInQuery) { - - return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, acceptParamInHeader, - DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, optionsParamInQuery, topicParamInQuery); + final String topicParamInQuery, + final String requestId) { + CpsValidator.validateNameCharacters(cmHandleId); + return getResourceDataResponse(cmHandleId, resourceIdentifier, + DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, optionsParamInQuery, topicParamInQuery, requestId); } @Override public Object getResourceDataPassThroughRunningForCmHandle(final String cmHandleId, final String resourceIdentifier, - final String acceptParamInHeader, final String optionsParamInQuery, - final String topicParamInQuery) { - return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, acceptParamInHeader, - DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING, optionsParamInQuery, topicParamInQuery); + final String topicParamInQuery, + final String requestId) { + CpsValidator.validateNameCharacters(cmHandleId); + return getResourceDataResponse(cmHandleId, resourceIdentifier, + DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING, optionsParamInQuery, topicParamInQuery, requestId); } @Override @@ -137,15 +135,16 @@ 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), - "Not able to " + operation + " resource data."); + dmiDataOperations.writeResourceDataPassThroughRunningFromDmi(cmHandleId, resourceIdentifier, operation, + requestData, dataType), operation); } @Override public Collection<ModuleReference> getYangResourcesModuleReferences(final String cmHandleId) { + CpsValidator.validateNameCharacters(cmHandleId); return cpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId); } @@ -160,180 +159,172 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService return cpsAdminService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, moduleNames); } + @Override + public Set<String> queryCmHandles(final CmHandleQueryApiParameters cmHandleQueryApiParameters) { + + cmHandleQueryApiParameters.getPublicProperties().forEach((key, value) -> { + if (Strings.isNullOrEmpty(key)) { + throw new DataValidationException("Invalid Query Parameter.", + "Missing property name - please supply a valid name."); + } + }); + + return cpsAdminService.queryCmHandles(jsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, + org.onap.cps.spi.model.CmHandleQueryParameters.class)); + } + /** * Retrieve cm handle details for a given cm handle. + * * @param cmHandleId cm handle identifier * @return cm handle details */ @Override public NcmpServiceCmHandle getNcmpServiceCmHandle(final String cmHandleId) { + CpsValidator.validateNameCharacters(cmHandleId); final NcmpServiceCmHandle ncmpServiceCmHandle = new NcmpServiceCmHandle(); final YangModelCmHandle yangModelCmHandle = yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId); final List<YangModelCmHandle.Property> dmiProperties = yangModelCmHandle.getDmiProperties(); final List<YangModelCmHandle.Property> publicProperties = yangModelCmHandle.getPublicProperties(); - ncmpServiceCmHandle.setCmHandleID(yangModelCmHandle.getId()); + ncmpServiceCmHandle.setCmHandleId(yangModelCmHandle.getId()); setDmiProperties(dmiProperties, ncmpServiceCmHandle); setPublicProperties(publicProperties, ncmpServiceCmHandle); return ncmpServiceCmHandle; } - private void setDmiProperties(final List<YangModelCmHandle.Property> dmiProperties, - final NcmpServiceCmHandle ncmpServiceCmHandle) { - final Map<String, String> dmiPropertiesMap = new LinkedHashMap<>(dmiProperties.size()); - asPropertiesMap(dmiProperties, dmiPropertiesMap); - ncmpServiceCmHandle.setDmiProperties(dmiPropertiesMap); - } - - private void setPublicProperties(final List<YangModelCmHandle.Property> publicProperties, - final NcmpServiceCmHandle ncmpServiceCmHandle) { - final Map<String, String> publicPropertiesMap = new LinkedHashMap<>(); - asPropertiesMap(publicProperties, publicPropertiesMap); - ncmpServiceCmHandle.setPublicProperties(publicPropertiesMap); - } - - private void asPropertiesMap(final List<YangModelCmHandle.Property> properties, - final Map<String, String> propertiesMap) { - for (final YangModelCmHandle.Property property: properties) { - propertiesMap.put(property.getName(), property.getValue()); - } - } - /** * 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); - } - - private static Object handleResponse(final ResponseEntity<?> responseEntity, - final String exceptionMessage) { - if (responseEntity.getStatusCode().is2xxSuccessful()) { - return responseEntity.getBody(); - } else { - throw new ServerNcmpException(exceptionMessage, - "DMI status code: " + responseEntity.getStatusCodeValue() - + ", DMI response body: " + responseEntity.getBody()); - } - } - - private void parseAndUpdateCmHandlesInDmiRegistration(final DmiPluginRegistration dmiPluginRegistration) { - networkCmProxyDataServicePropertyHandler.updateCmHandleProperties(dmiPluginRegistration.getUpdatedCmHandles()); - } - - 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, - cmHandleJsonData, NO_TIMESTAMP); - - for (final YangModelCmHandle yangModelCmHandle : yangModelCmHandlesList.getYangModelCmHandles()) { - syncModulesAndCreateAnchor(yangModelCmHandle); + public List<CmHandleRegistrationResponse> parseAndCreateCmHandlesInDmiRegistrationAndSyncModules( + final DmiPluginRegistration dmiPluginRegistration) { + List<CmHandleRegistrationResponse> cmHandleRegistrationResponses = new ArrayList<>(); + try { + cmHandleRegistrationResponses = dmiPluginRegistration.getCreatedCmHandles().stream() + .map(cmHandle -> + YangModelCmHandle.toYangModelCmHandle( + dmiPluginRegistration.getDmiPlugin(), + dmiPluginRegistration.getDmiDataPlugin(), + dmiPluginRegistration.getDmiModelPlugin(), cmHandle) + ) + .map(this::registerAndSyncNewCmHandle) + .collect(Collectors.toList()); + } catch (final DataValidationException dataValidationException) { + cmHandleRegistrationResponses.add(CmHandleRegistrationResponse.createFailureResponse(dmiPluginRegistration + .getCreatedCmHandles().stream() + .map(NcmpServiceCmHandle::getCmHandleId).findFirst().orElse(null), + RegistrationError.CM_HANDLE_INVALID_ID)); } + return cmHandleRegistrationResponses; } protected void syncModulesAndCreateAnchor(final YangModelCmHandle yangModelCmHandle) { - syncAndCreateSchemaSet(yangModelCmHandle); - createAnchor(yangModelCmHandle); + final String schemaSetName = moduleSyncService.syncAndCreateSchemaSet(yangModelCmHandle); + final String anchorName = yangModelCmHandle.getId(); + cpsAdminService.createAnchor(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, schemaSetName, + anchorName); } - private void parseAndRemoveCmHandlesInDmiRegistration(final DmiPluginRegistration dmiPluginRegistration) { - for (final String cmHandle : dmiPluginRegistration.getRemovedCmHandles()) { + protected List<CmHandleRegistrationResponse> parseAndRemoveCmHandlesInDmiRegistration( + final List<String> tobeRemovedCmHandles) { + final List<CmHandleRegistrationResponse> cmHandleRegistrationResponses = + new ArrayList<>(tobeRemovedCmHandles.size()); + for (final String cmHandle : tobeRemovedCmHandles) { try { - attemptToDeleteSchemaSetWithCascade(cmHandle); + CpsValidator.validateNameCharacters(cmHandle); + deleteSchemaSetWithCascade(cmHandle); cpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, "/dmi-registry/cm-handles[@id='" + cmHandle + "']", NO_TIMESTAMP); - } catch (final DataNodeNotFoundException e) { - log.warn("Datanode {} not deleted message {}", cmHandle, e.getMessage()); + cmHandleRegistrationResponses.add(CmHandleRegistrationResponse.createSuccessResponse(cmHandle)); + } catch (final DataNodeNotFoundException dataNodeNotFoundException) { + log.error("Unable to find dataNode for cmHandleId : {} , caused by : {}", + 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-handle id : {} , caused by : {}", + cmHandle, exception.getMessage()); + cmHandleRegistrationResponses.add( + CmHandleRegistrationResponse.createFailureResponse(cmHandle, exception)); } } + return cmHandleRegistrationResponses; } - private void attemptToDeleteSchemaSetWithCascade(final String schemaSetName) { + private void deleteSchemaSetWithCascade(final String schemaSetName) { try { cpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, schemaSetName, CASCADE_DELETE_ALLOWED); - } catch (final Exception e) { - log.warn("Schema set {} delete failed, reason {}", schemaSetName, e.getMessage()); + } catch (final SchemaSetNotFoundException schemaSetNotFoundException) { + log.warn("Schema set {} does not exist or already deleted", schemaSetName); } } - private void syncAndCreateSchemaSet(final YangModelCmHandle yangModelCmHandle) { - final Collection<ModuleReference> moduleReferencesFromCmHandle = - dmiModelOperations.getModuleReferences(yangModelCmHandle); - - final Collection<ModuleReference> identifiedNewModuleReferencesFromCmHandle = cpsModuleService - .identifyNewModuleReferences(moduleReferencesFromCmHandle); - - final Collection<ModuleReference> existingModuleReferencesFromCmHandle = - moduleReferencesFromCmHandle.stream().filter(moduleReferenceFromCmHandle -> - !identifiedNewModuleReferencesFromCmHandle.contains(moduleReferenceFromCmHandle) - ).collect(Collectors.toList()); + private Object getResourceDataResponse(final String cmHandleId, + final String resourceIdentifier, + final DmiOperations.DataStoreEnum dataStore, + final String optionsParamInQuery, + final String topicParamInQuery, + final String requestId) { + final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi( + cmHandleId, resourceIdentifier, optionsParamInQuery, dataStore, requestId, topicParamInQuery); + return handleResponse(responseEntity, OperationEnum.READ); + } - final Map<String, String> newModuleNameToContentMap; - if (identifiedNewModuleReferencesFromCmHandle.isEmpty()) { - newModuleNameToContentMap = new HashMap<>(); - } else { - newModuleNameToContentMap = dmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle, - identifiedNewModuleReferencesFromCmHandle); - } - cpsModuleService - .createSchemaSetFromModules(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, yangModelCmHandle.getId(), - newModuleNameToContentMap, existingModuleReferencesFromCmHandle); + private void setDmiProperties(final List<YangModelCmHandle.Property> dmiProperties, + final NcmpServiceCmHandle ncmpServiceCmHandle) { + final Map<String, String> dmiPropertiesMap = new LinkedHashMap<>(dmiProperties.size()); + asPropertiesMap(dmiProperties, dmiPropertiesMap); + ncmpServiceCmHandle.setDmiProperties(dmiPropertiesMap); } - private void createAnchor(final YangModelCmHandle yangModelCmHandle) { - cpsAdminService.createAnchor(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, yangModelCmHandle.getId(), - yangModelCmHandle.getId()); + private void setPublicProperties(final List<YangModelCmHandle.Property> publicProperties, + final NcmpServiceCmHandle ncmpServiceCmHandle) { + final Map<String, String> publicPropertiesMap = new LinkedHashMap<>(); + asPropertiesMap(publicProperties, publicPropertiesMap); + ncmpServiceCmHandle.setPublicProperties(publicPropertiesMap); } - private static boolean hasTopicParameter(final String topicName) { - if (topicName == null) { - return false; - } - if (TOPIC_NAME_PATTERN.matcher(topicName).matches()) { - return true; + private void asPropertiesMap(final List<YangModelCmHandle.Property> properties, + final Map<String, String> propertiesMap) { + for (final YangModelCmHandle.Property property: properties) { + propertiesMap.put(property.getName(), property.getValue()); } - throw new InvalidTopicException("Topic name " + topicName + " is invalid", "invalid topic"); } - private Map<String, Object> buildDmiResponse(final String requestId) { - final Map<String, Object> dmiResponseMap = new HashMap<>(); - dmiResponseMap.put("requestId", requestId); - return dmiResponseMap; + + private CmHandleRegistrationResponse registerAndSyncNewCmHandle(final YangModelCmHandle yangModelCmHandle) { + try { + 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); + syncModulesAndCreateAnchor(yangModelCmHandle); + return CmHandleRegistrationResponse.createSuccessResponse(yangModelCmHandle.getId()); + } catch (final AlreadyDefinedException alreadyDefinedException) { + return CmHandleRegistrationResponse.createFailureResponse( + yangModelCmHandle.getId(), RegistrationError.CM_HANDLE_ALREADY_EXIST); + } catch (final Exception exception) { + return CmHandleRegistrationResponse.createFailureResponse(yangModelCmHandle.getId(), exception); + } } - private Object validateTopicNameAndGetResourceData(final String cmHandleId, - final String resourceIdentifier, - final String acceptParamInHeader, - final DmiOperations.DataStoreEnum dataStore, - final String optionsParamInQuery, - final String topicParamInQuery) { - final boolean processAsynchronously = hasTopicParameter(topicParamInQuery); - if (processAsynchronously) { - final String resourceDataRequestId = UUID.randomUUID().toString(); - return ResponseEntity.status(HttpStatus.OK) - .body(buildDmiResponse(resourceDataRequestId)); + private static Object handleResponse(final ResponseEntity<?> responseEntity, final OperationEnum operation) { + if (responseEntity.getStatusCode().is2xxSuccessful()) { + return responseEntity.getBody(); + } else { + final String exceptionMessage = "Unable to " + operation.toString() + " resource data."; + throw new HttpClientRequestException(exceptionMessage, (String) responseEntity.getBody(), + responseEntity.getStatusCodeValue()); } - final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi( - cmHandleId, resourceIdentifier, optionsParamInQuery, acceptParamInHeader, - dataStore, NO_REQUEST_ID, NO_TOPIC); - return handleResponse(responseEntity, "Not able to get resource data."); } + }
\ No newline at end of file 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 ca2f578f46..aae2f209ae 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 @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,20 +29,26 @@ import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NCMP_DMI import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMESTAMP; import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsDataService; +import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse; +import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError; 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 @@ -61,23 +68,38 @@ public class NetworkCmProxyDataServicePropertyHandler { * * @param ncmpServiceCmHandles collection of ncmpServiceCmHandles */ - public void updateCmHandleProperties(final Collection<NcmpServiceCmHandle> ncmpServiceCmHandles) - throws DataNodeNotFoundException { + public List<CmHandleRegistrationResponse> updateCmHandleProperties( + final Collection<NcmpServiceCmHandle> ncmpServiceCmHandles) { + final List<CmHandleRegistrationResponse> cmHandleRegistrationResponses = new ArrayList<>(); for (final NcmpServiceCmHandle ncmpServiceCmHandle : ncmpServiceCmHandles) { + final String cmHandle = ncmpServiceCmHandle.getCmHandleId(); try { - final String cmHandleXpath = String.format(CM_HANDLE_XPATH_TEMPLATE, - ncmpServiceCmHandle.getCmHandleID()); + 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, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS); processUpdates(existingCmHandleDataNode, ncmpServiceCmHandle); + cmHandleRegistrationResponses.add(CmHandleRegistrationResponse.createSuccessResponse(cmHandle)); } catch (final DataNodeNotFoundException e) { log.error("Unable to find dataNode for cmHandleId : {} , caused by : {}", - ncmpServiceCmHandle.getCmHandleID(), - e.getMessage()); - throw e; + 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 cmHandle : {} , caused by : {}", + cmHandle, exception.getMessage()); + cmHandleRegistrationResponses.add( + CmHandleRegistrationResponse.createFailureResponse(cmHandle, exception)); } } + return cmHandleRegistrationResponses; } private void processUpdates(final DataNode existingCmHandleDataNode, final NcmpServiceCmHandle incomingCmHandle) { diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java index 94faa557fa..f1bb95f34e 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,43 +21,32 @@ package org.onap.cps.ncmp.api.impl.client; +import lombok.AllArgsConstructor; import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration.DmiProperties; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component +@AllArgsConstructor public class DmiRestClient { private RestTemplate restTemplate; private DmiProperties dmiProperties; - /** - * Constructor injection for DmiRestClient objects. - * - * @param restTemplate the rest template - * @param dmiProperties the DMI properties - */ - public DmiRestClient(final RestTemplate restTemplate, final DmiProperties dmiProperties) { - this.restTemplate = restTemplate; - this.dmiProperties = dmiProperties; - } /** * Sends POST operation to DMI with json body containing module references. * @param dmiResourceUrl dmi resource url * @param jsonData json data body - * @param httpHeaders http headers * @return response entity of type String */ public ResponseEntity<Object> postOperationWithJsonData(final String dmiResourceUrl, - final String jsonData, - final HttpHeaders httpHeaders) { - final var httpEntity = new HttpEntity<>(jsonData, configureHttpHeaders(httpHeaders)); + final String jsonData) { + final var httpEntity = new HttpEntity<>(jsonData, configureHttpHeaders(new HttpHeaders())); return restTemplate.postForEntity(dmiResourceUrl, httpEntity, Object.class); } @@ -65,15 +55,4 @@ public class DmiRestClient { httpHeaders.setContentType(MediaType.APPLICATION_JSON); return httpHeaders; } - - /** - * Sends POST operation to DMI. - * @param dmiResourceUrl dmi resource url - * @param httpHeaders http headers - * @return response entity of type String - */ - public ResponseEntity<Object> postOperation(final String dmiResourceUrl, final HttpHeaders httpHeaders) { - final var httpEntity = new HttpEntity<>(configureHttpHeaders(httpHeaders)); - return restTemplate.exchange(dmiResourceUrl, HttpMethod.POST, httpEntity, Object.class); - } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java new file mode 100644 index 0000000000..9d307e5d2e --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java @@ -0,0 +1,45 @@ +/* + * ============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.ncmp.api.impl.exception; + +import lombok.Getter; + +/** + * Http Client Request exception for passthrough scenarios. + */ +@Getter +public class HttpClientRequestException extends NcmpException { + + private static final long serialVersionUID = 6659897770659834797L; + final Integer httpStatus; + + /** + * Constructor to form exception for passthrough scenarios. + * + * @param message message details from NCMP + * @param details response body from the client available as details + * @param httpStatus http status code from the client + */ + public HttpClientRequestException(final String message, final String details, final Integer httpStatus) { + super(message, details); + this.httpStatus = httpStatus; + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java index 68de9d5c6b..ad85edde7b 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021-2022 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,8 +29,8 @@ import org.onap.cps.ncmp.api.impl.client.DmiRestClient; import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; +import org.onap.cps.utils.CpsValidator; import org.onap.cps.utils.JsonObjectMapper; -import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; @@ -58,7 +59,6 @@ public class DmiDataOperations extends DmiOperations { * @param cmHandleId network resource identifier * @param resourceId resource identifier * @param optionsParamInQuery options query - * @param acceptParamInHeader accept parameter * @param dataStore data store enum * @param requestId requestId for async responses * @param topicParamInQuery topic name for (triggering) async responses @@ -67,10 +67,10 @@ public class DmiDataOperations extends DmiOperations { public ResponseEntity<Object> getResourceDataFromDmi(final String cmHandleId, final String resourceId, final String optionsParamInQuery, - final String acceptParamInHeader, final DataStoreEnum dataStore, final String requestId, final String topicParamInQuery) { + CpsValidator.validateNameCharacters(cmHandleId); final YangModelCmHandle yangModelCmHandle = yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId); final DmiRequestBody dmiRequestBody = DmiRequestBody.builder() @@ -79,13 +79,11 @@ public class DmiDataOperations extends DmiOperations { .build(); dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties()); final String jsonBody = jsonObjectMapper.asJsonString(dmiRequestBody); - - final var dmiResourceDataUrl = dmiServiceUrlBuilder.getDmiDatastoreUrl( + final String dmiResourceDataUrl = dmiServiceUrlBuilder.getDmiDatastoreUrl( dmiServiceUrlBuilder.populateQueryParams(resourceId, optionsParamInQuery, topicParamInQuery), dmiServiceUrlBuilder.populateUriVariables( yangModelCmHandle, cmHandleId, dataStore)); - final var httpHeaders = prepareHeader(acceptParamInHeader); - return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonBody, httpHeaders); + return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonBody); } /** @@ -104,6 +102,7 @@ public class DmiDataOperations extends DmiOperations { final OperationEnum operation, final String requestData, final String dataType) { + CpsValidator.validateNameCharacters(cmHandleId); final YangModelCmHandle yangModelCmHandle = yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId); final DmiRequestBody dmiRequestBody = DmiRequestBody.builder() @@ -114,10 +113,10 @@ public class DmiDataOperations extends DmiOperations { dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties()); final String jsonBody = jsonObjectMapper.asJsonString(dmiRequestBody); final String dmiUrl = - dmiServiceUrlBuilder.getDmiDatastoreUrl(dmiServiceUrlBuilder.populateQueryParams(resourceId, - null, null), - dmiServiceUrlBuilder.populateUriVariables(yangModelCmHandle, cmHandleId, PASSTHROUGH_RUNNING)); - return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonBody, new HttpHeaders()); + dmiServiceUrlBuilder.getDmiDatastoreUrl(dmiServiceUrlBuilder.populateQueryParams(resourceId, + null, null), + dmiServiceUrlBuilder.populateUriVariables(yangModelCmHandle, cmHandleId, PASSTHROUGH_RUNNING)); + return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonBody); } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java index d79988e2e0..b033af87cd 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021-2022 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +37,6 @@ import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; import org.onap.cps.ncmp.api.models.YangResource; import org.onap.cps.spi.model.ModuleReference; import org.onap.cps.utils.JsonObjectMapper; -import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; @@ -107,7 +107,7 @@ public class DmiModelOperations extends DmiOperations { final String cmHandle, final String resourceName) { final String dmiResourceDataUrl = getDmiResourceUrl(dmiServiceName, cmHandle, resourceName); - return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonData, new HttpHeaders()); + return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonData); } private static String getRequestBodyToFetchYangResources(final Collection<ModuleReference> newModuleReferences, diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java index 75ba91b4f7..745007bd44 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021-2022 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +27,6 @@ import org.onap.cps.ncmp.api.impl.client.DmiRestClient; import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; import org.onap.cps.utils.JsonObjectMapper; -import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; @RequiredArgsConstructor @@ -56,10 +56,5 @@ public class DmiOperations { .buildAndExpand(dmiServiceName, dmiProperties.getDmiBasePath(), cmHandle, resourceName).toUriString(); } - static HttpHeaders prepareHeader(final String acceptParam) { - final var httpHeaders = new HttpHeaders(); - httpHeaders.set(HttpHeaders.ACCEPT, acceptParam); - return httpHeaders; - } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetriever.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetriever.java index 6b6bdf5be4..0efe8d5b62 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetriever.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetriever.java @@ -28,6 +28,7 @@ import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.model.DataNode; +import org.onap.cps.utils.CpsValidator; import org.springframework.stereotype.Component; /** @@ -48,9 +49,10 @@ public class YangModelCmHandleRetriever { * @return yang model cm handle */ public YangModelCmHandle getDmiServiceNamesAndProperties(final String cmHandleId) { + CpsValidator.validateNameCharacters(cmHandleId); final DataNode cmHandleDataNode = getCmHandleDataNode(cmHandleId); final NcmpServiceCmHandle ncmpServiceCmHandle = new NcmpServiceCmHandle(); - ncmpServiceCmHandle.setCmHandleID(cmHandleId); + ncmpServiceCmHandle.setCmHandleId(cmHandleId); populateCmHandleProperties(cmHandleDataNode, ncmpServiceCmHandle); return YangModelCmHandle.toYangModelCmHandle( String.valueOf(cmHandleDataNode.getLeaves().get("dmi-service-name")), diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java index b60aac9518..b679107251 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java @@ -30,6 +30,7 @@ import org.apache.logging.log4j.util.TriConsumer; import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; import org.onap.cps.ncmp.api.impl.operations.DmiOperations; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; +import org.onap.cps.utils.CpsValidator; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -70,25 +71,26 @@ public class DmiServiceUrlBuilder { .pathSegment("{dmiBasePath}") .pathSegment("v1") .pathSegment("ch") - .pathSegment("{cmHandle}"); + .pathSegment("{cmHandleId}"); } /** * This method populates uri variables. * * @param yangModelCmHandle get dmi service name - * @param cmHandle cm handle name for dmi registration + * @param cmHandleId cm handle id for dmi registration * @return {@code String} dmi service url as string */ public Map<String, Object> populateUriVariables(final YangModelCmHandle yangModelCmHandle, - final String cmHandle, + final String cmHandleId, final DmiOperations.DataStoreEnum dataStore) { + CpsValidator.validateNameCharacters(cmHandleId); final Map<String, Object> uriVariables = new HashMap<>(); final String dmiBasePath = dmiProperties.getDmiBasePath(); uriVariables.put("dmiServiceName", yangModelCmHandle.resolveDmiServiceName(DATA)); uriVariables.put("dmiBasePath", dmiBasePath); - uriVariables.put("cmHandle", cmHandle); + uriVariables.put("cmHandleId", cmHandleId); uriVariables.put("dataStore", dataStore.getValue()); return uriVariables; } 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..fd3528187e 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; @@ -33,6 +35,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.onap.cps.ncmp.api.impl.operations.RequiredDmiService; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; +import org.onap.cps.utils.CpsValidator; /** * Cm Handle which follows the Yang resource dmi registry model when persisting data to DMI or the DB. @@ -41,6 +44,7 @@ import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; @Getter @Setter @NoArgsConstructor +@JsonInclude(Include.NON_NULL) public class YangModelCmHandle { private String id; @@ -72,8 +76,9 @@ public class YangModelCmHandle { final String dmiDataServiceName, final String dmiModelServiceName, final NcmpServiceCmHandle ncmpServiceCmHandle) { + CpsValidator.validateNameCharacters(ncmpServiceCmHandle.getCmHandleId()); final YangModelCmHandle yangModelCmHandle = new YangModelCmHandle(); - yangModelCmHandle.setId(ncmpServiceCmHandle.getCmHandleID()); + yangModelCmHandle.setId(ncmpServiceCmHandle.getCmHandleId()); yangModelCmHandle.setDmiServiceName(dmiServiceName); yangModelCmHandle.setDmiDataServiceName(dmiDataServiceName); yangModelCmHandle.setDmiModelServiceName(dmiModelServiceName); 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/inventory/sync/ModuleSyncService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncService.java new file mode 100644 index 0000000000..1d00f0dc6b --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncService.java @@ -0,0 +1,84 @@ +/* + * ============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.ncmp.api.inventory.sync; + +import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.api.CpsModuleService; +import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations; +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; +import org.onap.cps.spi.model.ModuleReference; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ModuleSyncService { + + private final DmiModelOperations dmiModelOperations; + private final CpsModuleService cpsModuleService; + + /** + * This method registers a cm handle and initiates modules sync. + * + * @param yangModelCmHandle the yang model of cm handle. + * @return schemaSetName the name of the schema set (same as cm handle name). + */ + public String syncAndCreateSchemaSet(final YangModelCmHandle yangModelCmHandle) { + + final Collection<ModuleReference> moduleReferencesFromCmHandle = + dmiModelOperations.getModuleReferences(yangModelCmHandle); + + final Collection<ModuleReference> identifiedNewModuleReferencesFromCmHandle = cpsModuleService + .identifyNewModuleReferences(moduleReferencesFromCmHandle); + + final Collection<ModuleReference> existingModuleReferencesFromCmHandle = + moduleReferencesFromCmHandle.stream().filter(moduleReferenceFromCmHandle -> + !identifiedNewModuleReferencesFromCmHandle.contains(moduleReferenceFromCmHandle) + ).collect(Collectors.toList()); + + final Map<String, String> newModuleNameToContentMap; + if (identifiedNewModuleReferencesFromCmHandle.isEmpty()) { + newModuleNameToContentMap = new HashMap<>(); + } else { + newModuleNameToContentMap = dmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle, + identifiedNewModuleReferencesFromCmHandle); + } + return createSchemaSet(yangModelCmHandle, existingModuleReferencesFromCmHandle, newModuleNameToContentMap); + } + + private String createSchemaSet(final YangModelCmHandle yangModelCmHandle, + final Collection<ModuleReference> existingModuleReferencesFromCmHandle, + final Map<String, String> newModuleNameToContentMap) { + final String schemaSetName = yangModelCmHandle.getId(); + cpsModuleService + .createSchemaSetFromModules(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, schemaSetName, + newModuleNameToContentMap, existingModuleReferencesFromCmHandle); + return schemaSetName; + } + +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java new file mode 100644 index 0000000000..3f584ed153 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java @@ -0,0 +1,41 @@ +/* + * ============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.ncmp.api.models; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.Map; +import javax.validation.Valid; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@JsonInclude(Include.NON_NULL) +public class CmHandleQueryApiParameters { + + @JsonProperty("publicCmHandleProperties") + @Valid + private Map<String, String> publicProperties = Collections.emptyMap(); + +} 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 new file mode 100644 index 0000000000..1da2aa9430 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java @@ -0,0 +1,88 @@ +/* + * ============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. + * 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.models; + +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@Builder +public class CmHandleRegistrationResponse { + + private final String cmHandle; + private final Status status; + private RegistrationError registrationError; + private String errorText; + + /** + * Creates a failure response based on exception. + * + * @param cmHandle cmHandle + * @param exception exception + * @return CmHandleRegistrationResponse + */ + public static CmHandleRegistrationResponse createFailureResponse(final String cmHandle, final Exception exception) { + return CmHandleRegistrationResponse.builder() + .cmHandle(cmHandle) + .status(Status.FAILURE) + .registrationError(RegistrationError.UNKNOWN_ERROR) + .errorText(exception.getMessage()).build(); + } + + /** + * Creates a failure response based on registration error. + * + * @param cmHandle cmHandle + * @param registrationError registrationError + * @return CmHandleRegistrationResponse + */ + public static CmHandleRegistrationResponse createFailureResponse(final String cmHandle, + final RegistrationError registrationError) { + return CmHandleRegistrationResponse.builder().cmHandle(cmHandle) + .status(Status.FAILURE) + .registrationError(registrationError) + .errorText(registrationError.errorText) + .build(); + } + + public static CmHandleRegistrationResponse createSuccessResponse(final String cmHandle) { + return CmHandleRegistrationResponse.builder().cmHandle(cmHandle) + .status(Status.SUCCESS).build(); + } + + public enum Status { + SUCCESS, FAILURE; + } + + @RequiredArgsConstructor + 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_INVALID_ID("03", "cm-handle has an invalid character(s) in id"); + + public final String errorCode; + public final String errorText; + + } +}
\ No newline at end of file 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 new file mode 100644 index 0000000000..8a3d26414a --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java @@ -0,0 +1,34 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Bell Canada + * ================================================================================ + * 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.models; + +import java.util.Collections; +import java.util.List; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class DmiPluginRegistrationResponse { + 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/main/java/org/onap/cps/ncmp/api/models/NcmpServiceCmHandle.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/NcmpServiceCmHandle.java index 938127020c..6811b59e00 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/NcmpServiceCmHandle.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/NcmpServiceCmHandle.java @@ -39,7 +39,7 @@ import org.springframework.validation.annotation.Validated; @NoArgsConstructor public class NcmpServiceCmHandle { - private String cmHandleID; + private String cmHandleId; @JsonSetter(nulls = Nulls.AS_EMPTY) private Map<String, String> dmiProperties = Collections.emptyMap(); 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 e410463afa..5683d57e54 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 @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021-2022 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +21,8 @@ package org.onap.cps.ncmp.api.impl -import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.ObjectMapper +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 @@ -29,20 +30,29 @@ import org.onap.cps.ncmp.api.impl.exception.DmiRequestException import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations 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.ncmp.api.inventory.sync.ModuleSyncService +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.utils.JsonObjectMapper 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 { @Shared - def ncmpServiceCmHandle = new NcmpServiceCmHandle() + def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'some-cm-handle-id') @Shared def cmHandlesArray = ['cmHandle001'] @@ -55,104 +65,57 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification { def mockDmiDataOperations = Mock(DmiDataOperations) def mockNetworkCmProxyDataServicePropertyHandler = Mock(NetworkCmProxyDataServicePropertyHandler) def mockYangModelCmHandleRetriever = Mock(YangModelCmHandleRetriever) + def mockModuleSyncService = Mock(ModuleSyncService) def noTimestamp = null + def objectUnderTest = getObjectUnderTestWithModelSyncDisabled() - def 'Register or re-register a DMI Plugin for the given cm-handle(s) with #scenario process.'() { - given: 'a registration' - def objectUnderTest = getObjectUnderTestWithModelSyncDisabled() - 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' - 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) - 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 + def 'DMI Registration: Create, Update & Delete operations are processed in the right order'() { + 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']) + when: 'registration is processed' + objectUnderTest.updateDmiRegistrationAndSyncModule(dmiRegistration) + // Spock validated invocation order between multiple then blocks + then: 'cm-handles are removed first' + 1 * objectUnderTest.parseAndRemoveCmHandlesInDmiRegistration(*_) + then: 'cm-handles are created' + 1 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(*_) + then: 'cm-handles are updated' + 1 * mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_) } - def 'Register a DMI Plugin for the given cm-handle(s) without DMI properties.'() { - 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":[]}]}' - 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 '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 'Register a DMI Plugin for a given cm-handle(s) with JSON processing errors during process.'() { - given: 'a registration without cm-handle properties ' - NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled() - 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 'Register a DMI Plugin for the given cm-handle(s) with no data found during delete process.'() { - given: 'a registration without cm-handle properties ' - NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled() - def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:'some-plugin') - dmiPluginRegistration.removedCmHandles = ['some cm handle'] - and: 'an json processing exception occurs during delete process' - mockCpsDataService.deleteListOrListElement(*_) >> { throw (new DataNodeNotFoundException('','')) } - when: 'registration is updated and modules are synced' - objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration) - then: 'no exception is thrown' - noExceptionThrown() } - def 'Register a DMI Plugin for the given cm-handle(s) with no schema set found during delete process.'() { - given: 'a registration' - def objectUnderTest = getObjectUnderTestWithModelSyncDisabled() - def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:'my-server') - dmiPluginRegistration.removedCmHandles = cmHandlesArray - and: 'an exception occurs during delete schema set process' - mockCpsModuleService.deleteSchemaSet(_,_,_) >> { throw (new Exception('')) } - when: 'registration is updated and modules are synced' - objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration) - then: 'delete list or list element is still called' - 1 * mockCpsDataService.deleteListOrListElement(_,_,_,_) - } - - def 'Dmi plugin registration with #scenario'() { + def 'Create CM-handle Validation: Registration with valid Service names: #scenario'() { given: 'a registration ' - def objectUnderTest = getObjectUnderTestWithModelSyncDisabled() - def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:dmiPlugin, dmiModelPlugin:dmiModelPlugin, - dmiDataPlugin:dmiDataPlugin) + 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) @@ -165,11 +128,10 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification { 'data & model using same service' | '' | 'service1' | 'service1' } - def 'Invalid DMI plugin registration with #scenario'() { + def 'Create CM-handle Validation: Invalid DMI plugin service name with #scenario'() { given: 'a registration ' - def objectUnderTest = getObjectUnderTestWithModelSyncDisabled() - def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:dmiPlugin, dmiModelPlugin:dmiModelPlugin, - dmiDataPlugin:dmiDataPlugin) + 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) @@ -179,37 +141,254 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification { 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 | 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' + def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server') + dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleId: 'cmhandle', dmiProperties: dmiProperties, publicProperties: publicProperties)] + when: 'registration is updated' + 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 'Exception thrown on CM-Handle registration update request'() { - given: 'a CM-handle registration' - def objectUnderTest = getObjectUnderTestWithModelSyncDisabled() - and: 'dmi plugin registration input update request' - def dmiPluginReg = new DmiPluginRegistration(); - dmiPluginReg.dmiPlugin = 'onap.dmap.plugin'; - dmiPluginReg.updatedCmHandles = [new NcmpServiceCmHandle(cmHandleID: 'unknownHandle')] - and: 'update data node leaves is unable to find data node' - mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_) >> { throw new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry') } - when: 'update dmi registration is called' - objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginReg) - then: 'data validation exception is thrown' - def exceptionThrown = thrown(DataValidationException.class) - assert exceptionThrown.getDetails().contains('DataNode not found') + 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'() { + given: 'a registration to update CmHandles' + def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server', + updatedCmHandles: [{}]) + 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 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() == 4 + assert response.getUpdatedCmHandles().containsAll(updateOperationResponse) + } + + def 'Remove CmHandle Successfully: #scenario'() { + given: 'a registration' + def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server', + removedCmHandles: ['cmhandle']) + and: '#scenario' + mockCpsModuleService.deleteSchemaSet(_, 'cmhandle', CASCADE_DELETE_ALLOWED) >> + { if (!schemaSetExist) { throw new SchemaSetNotFoundException("", "") } } + when: 'registration is updated to delete cmhandle' + def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration) + then: 'delete list or list element is called' + 1 * mockCpsDataService.deleteListOrListElement(_, _, _, _) + and: 'successful response is received' + assert response.getRemovedCmHandles().size() == 1 + with(response.getRemovedCmHandles().get(0)) { + assert it.status == Status.SUCCESS + assert it.cmHandle == 'cmhandle' + } + where: + scenario | schemaSetExist + 'schema-set exists and can be deleted successfully' | true + 'schema-set does not exist' | false + } + + def 'Remove CmHandle: All cm-handles delete requests are processed'() { + given: 'a registration with three cm-handles to be deleted' + def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server', + removedCmHandles: ['cmhandle1', 'cmhandle2', 'cmhandle3']) + and: 'cm-handle deletion is successful for 1st and 3rd; failed for 2nd' + mockCpsDataService.deleteListOrListElement(_, _, _, _) >> {} >> { throw new RuntimeException("Failed") } >> {} + when: 'registration is updated to delete cmhandles' + def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration) + then: 'a response is received for all cm-handles' + response.getRemovedCmHandles().size() == 3 + and: '1st and 3rd cm-handle deletes successfully' + with(response.getRemovedCmHandles().get(0)) { + assert it.status == Status.SUCCESS + assert it.cmHandle == 'cmhandle1' + } + with(response.getRemovedCmHandles().get(2)) { + assert it.status == Status.SUCCESS + assert it.cmHandle == 'cmhandle3' + } + and: '2nd cm-handle deletion fails' + with(response.getRemovedCmHandles().get(1)) { + assert it.status == Status.FAILURE + assert it.registrationError == UNKNOWN_ERROR + assert it.errorText == 'Failed' + assert it.cmHandle == 'cmhandle2' + } + } + + def 'Remove CmHandle Error Handling: Schema Set Deletion failed'() { + given: 'a registration' + def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server', + removedCmHandles: ['cmhandle']) + and: 'schema set deletion failed with unknown error' + mockCpsModuleService.deleteSchemaSet(_, _, _) >> { throw new RuntimeException('Failed') } + when: 'registration is updated to delete cmhandle' + def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration) + then: 'no exception is thrown' + noExceptionThrown() + and: 'cm-handle is not deleted' + 0 * mockCpsDataService.deleteListOrListElement(_, _, _, _) + and: 'a failure response is received' + assert response.getRemovedCmHandles().size() == 1 + with(response.getRemovedCmHandles().get(0)) { + assert it.status == Status.FAILURE + assert it.cmHandle == 'cmhandle' + assert it.errorText == 'Failed' + assert it.registrationError == UNKNOWN_ERROR + } + } + + def 'Remove CmHandle Error Handling: #scenario'() { + given: 'a registration' + def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server', + removedCmHandles: ['cmhandle']) + and: 'cm-handle deletion throws exception' + mockCpsDataService.deleteListOrListElement(_, _, _, _) >> { throw deleteListElementException } + when: 'registration is updated to delete cmhandle' + def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration) + then: 'no exception is thrown' + noExceptionThrown() + and: 'a failure response is received' + assert response.getRemovedCmHandles().size() == 1 + with(response.getRemovedCmHandles().get(0)) { + assert it.status == Status.FAILURE + assert it.cmHandle == 'cmhandle' + assert it.registrationError == expectedError + assert it.errorText == expectedErrorText + } + where: + 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, + mockCpsModuleService, mockCpsAdminService, mockNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever, mockModuleSyncService)) + } } 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 c21d7e7742..7629500db0 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 @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2021 Bell Canada + * Modifications Copyright (C) 2021-2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,13 @@ package org.onap.cps.ncmp.api.impl -import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException +import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import org.onap.cps.ncmp.api.models.DmiPluginRegistration +import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle +import org.onap.cps.spi.exceptions.DataValidationException +import org.onap.cps.ncmp.api.inventory.sync.ModuleSyncService import spock.lang.Shared import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL @@ -33,14 +37,12 @@ import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.READ import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.UPDATE -import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations import org.onap.cps.utils.JsonObjectMapper import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.api.CpsAdminService import org.onap.cps.api.CpsDataService import org.onap.cps.api.CpsModuleService -import org.onap.cps.ncmp.api.impl.exception.ServerNcmpException import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations import org.onap.cps.spi.FetchDescendantsOption import org.onap.cps.spi.model.DataNode @@ -54,17 +56,21 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { def mockCpsModuleService = Mock(CpsModuleService) def mockCpsAdminService = Mock(CpsAdminService) def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper())) - def mockDmiModelOperations = Mock(DmiModelOperations) def mockDmiDataOperations = Mock(DmiDataOperations) def nullNetworkCmProxyDataServicePropertyHandler = null def mockYangModelCmHandleRetriever = Mock(YangModelCmHandleRetriever) + def mockModuleSyncService = Mock(ModuleSyncService) + def mockDmiPluginRegistration = Mock(DmiPluginRegistration) + def NO_TOPIC = null def NO_REQUEST_ID = null @Shared def OPTIONS_PARAM = '(a=1,b=2)' + @Shared + def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'some-cm-handle-id') - def objectUnderTest = new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations, - mockCpsModuleService, mockCpsAdminService, nullNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever) + def objectUnderTest = new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, + mockCpsModuleService, mockCpsAdminService, nullNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever, mockModuleSyncService) def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']" @@ -84,6 +90,17 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { >> { new ResponseEntity<>(HttpStatus.CREATED) } } + def 'Write resource data for pass-through running from DMI using an invalid id.'() { + when: 'write resource data is called' + objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('invalid cm handle name', + 'testResourceId', CREATE, + '{some-json}', 'application/json') + then: 'exception is thrown' + thrown(DataValidationException.class) + and: 'DMI is not invoked' + 0 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi(_, _, _, _, _) + } + def 'Write resource data for pass-through running from DMI using POST "not found" response (from DMI).'() { given: 'cpsDataService returns valid dataNode' mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', @@ -98,9 +115,9 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { 'testResourceId', CREATE, '{some-json}', 'application/json') then: 'exception is thrown' - def exceptionThrown = thrown(ServerNcmpException.class) - and: 'details contains (not found) error code: 404' - exceptionThrown.details.contains('404') + def exceptionThrown = thrown(HttpClientRequestException.class) + and: 'http status (not found) error code: 404' + exceptionThrown.httpStatus == HttpStatus.NOT_FOUND.value() } def 'Get resource data for pass-through operational from DMI.'() { @@ -112,20 +129,30 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { 'testCmHandle', 'testResourceId', OPTIONS_PARAM, - 'testAcceptParam', PASSTHROUGH_OPERATIONAL, NO_REQUEST_ID, NO_TOPIC) >> new ResponseEntity<>('dmi-response', HttpStatus.OK) when: 'get resource data operational for cm-handle is called' def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle', 'testResourceId', - 'testAcceptParam', OPTIONS_PARAM, - NO_TOPIC) + NO_TOPIC, + NO_REQUEST_ID) then: 'DMI returns a json response' response == 'dmi-response' } + def 'Get resource data for pass-through operational from DMI with invalid name.'() {\ + when: 'get resource data operational for cm-handle is called' + objectUnderTest.getResourceDataOperationalForCmHandle('invalid test cm handle', + 'testResourceId', + OPTIONS_PARAM, + NO_TOPIC, + NO_REQUEST_ID) + then: 'A data validation Exception is thrown' + thrown(DataValidationException) + } + def 'Get resource data for pass-through operational from DMI with Json Processing Exception.'() { given: 'cps data service returns valid data node' mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', @@ -138,12 +165,13 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { when: 'get resource data is called' objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle', 'testResourceId', - 'testAcceptParam', OPTIONS_PARAM, - NO_TOPIC) - then: 'exception is thrown with the expected details' - def exceptionThrown = thrown(ServerNcmpException.class) - exceptionThrown.details == 'DMI status code: 404, DMI response body: NOK-json' + NO_TOPIC, + NO_REQUEST_ID) + then: 'exception is thrown with the expected response code and details' + def exceptionThrown = thrown(HttpClientRequestException.class) + exceptionThrown.details.contains('NOK-json') + exceptionThrown.httpStatus == HttpStatus.NOT_FOUND.value() } def 'Get resource data for pass-through operational from DMI return NOK response.'() { @@ -154,7 +182,6 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { mockDmiDataOperations.getResourceDataFromDmi('testCmHandle', 'testResourceId', OPTIONS_PARAM, - 'testAcceptParam', PASSTHROUGH_OPERATIONAL, NO_REQUEST_ID, NO_TOPIC) @@ -162,12 +189,13 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { when: 'get resource data is called' objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle', 'testResourceId', - 'testAcceptParam', OPTIONS_PARAM, - NO_TOPIC) + NO_TOPIC, + NO_REQUEST_ID) then: 'exception is thrown' - def exceptionThrown = thrown(ServerNcmpException.class) - and: 'details contains the original response' + def exceptionThrown = thrown(HttpClientRequestException.class) + and: 'details contain the original response' + exceptionThrown.httpStatus == HttpStatus.NOT_FOUND.value() exceptionThrown.details.contains('NOK-json') } @@ -179,20 +207,30 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { mockDmiDataOperations.getResourceDataFromDmi('testCmHandle', 'testResourceId', OPTIONS_PARAM, - 'testAcceptParam', PASSTHROUGH_RUNNING, NO_REQUEST_ID, NO_TOPIC) >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) when: 'get resource data is called' def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle', 'testResourceId', - 'testAcceptParam', OPTIONS_PARAM, - NO_TOPIC) + NO_TOPIC, + NO_REQUEST_ID) then: 'get resource data returns expected response' response == '{dmi-response}' } + def 'Get resource data for pass-through running from DMI with invalid name.'() { + when: 'get resource data operational for cm-handle is called' + objectUnderTest.getResourceDataPassThroughRunningForCmHandle('invalid test cm handle', + 'testResourceId', + OPTIONS_PARAM, + NO_TOPIC, + NO_REQUEST_ID) + then: 'A data validation Exception is thrown' + thrown(DataValidationException) + } + def 'Get resource data for pass-through running from DMI return NOK response.'() { given: 'cpsDataService returns valid dataNode' mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', @@ -201,7 +239,6 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { mockDmiDataOperations.getResourceDataFromDmi('testCmHandle', 'testResourceId', OPTIONS_PARAM, - 'testAcceptParam', PASSTHROUGH_RUNNING, NO_REQUEST_ID, NO_TOPIC) @@ -209,86 +246,30 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { when: 'get resource data is called' objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle', 'testResourceId', - 'testAcceptParam', OPTIONS_PARAM, - NO_TOPIC) + NO_TOPIC, + NO_REQUEST_ID) then: 'exception is thrown' - def exceptionThrown = thrown(ServerNcmpException.class) - and: 'details contains the original response' + def exceptionThrown = thrown(HttpClientRequestException.class) + and: 'details contain the original response' exceptionThrown.details.contains('NOK-json') - } - - def 'DMI Operational data request with #scenario'() { - given: 'cps data service returns valid data node' - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', - cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode - and: 'dmi data operation returns valid response and data' - mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, NO_REQUEST_ID, NO_TOPIC) - >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) - when: 'get resource data is called data operational with blank topic' - def responseData = objectUnderTest.getResourceDataOperationalForCmHandle('', '', - '', '', emptyTopic) - then: 'a invalid topic exception is thrown' - thrown(InvalidTopicException) - where: 'the following parameters are used' - scenario | emptyTopic - 'no topic value in url' | '' - 'empty topic value in url' | '\"\"' - 'blank topic value in url' | ' ' - 'invalid non-empty topic value in url' | '1_5_*_#' - } - - def 'Get resource data for data operational from DMI with valid topic i.e. async request.'() { - given: 'cps data service returns valid data node' - mockCpsDataService.getDataNode(*_) >> dataNode - and: 'dmi data operation returns valid response and data' - mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, _, 'my-topic-name') - >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) - when: 'get resource data is called for data operational with valid topic' - def responseData = objectUnderTest.getResourceDataOperationalForCmHandle('', '', '', '', 'my-topic-name') - then: 'non empty request id is generated' - assert responseData.body.requestId.length() > 0 - } - - def 'Get resource data for pass through running from DMI with valid topic async request.'() { - given: 'cps data service returns valid data node' - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', - cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode - and: 'dmi data operation returns valid response and data' - mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, _, 'my-topic-name') - >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) - when: 'get resource data is called for data operational with valid topic' - def responseData = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('', - '', '', OPTIONS_PARAM, 'my-topic-name') - then: 'non empty request id is generated' - assert responseData.body.requestId.length() > 0 - } - - def 'DMI pass through running data request with #scenario'() { - given: 'cps data service returns valid data node' - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', - cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode - and: 'dmi data operation returns valid response and data' - mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, NO_REQUEST_ID, NO_TOPIC) - >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) - when: 'get resource data is called for data operational with valid topic' - def responseData = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('', - '', '', '', emptyTopic) - then: 'a invalid topic exception is thrown' - thrown(InvalidTopicException) - where: 'the following parameters are used' - scenario | emptyTopic - 'no topic value in url' | '' - 'empty topic value in url' | '\"\"' - 'blank topic value in url' | ' ' - 'invalid non-empty topic value in url' | '1_5_*_#' + exceptionThrown.httpStatus == HttpStatus.NOT_FOUND.value() } 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 'Getting Yang Resources with an invalid #scenario.'() { + when: 'yang resources is called' + objectUnderTest.getYangResourcesModuleReferences('invalid cm handle with spaces') + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'CPS module services is not invoked' + 0 * mockCpsModuleService.getYangResourcesModuleReferences(_, _) } def 'Get cm handle identifiers for the given module names.'() { @@ -308,12 +289,21 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { when: 'getting cm handle details for a given cm handle id from ncmp service' def result = objectUnderTest.getNcmpServiceCmHandle('Some-Cm-Handle') then: 'the result returns the correct data' - result.cmHandleID == 'Some-Cm-Handle' + result.cmHandleId == 'Some-Cm-Handle' result.dmiProperties ==[ Book:'Romance Novel' ] result.publicProperties == [ "Public Book":'Public Romance Novel' ] } + def 'Get a cm handle with an invalid id.'() { + when: 'getting cm handle details for a given cm handle id with an invalid name' + objectUnderTest.getNcmpServiceCmHandle('invalid cm handle with spaces') + then: 'an exception is thrown' + thrown(DataValidationException) + and: 'the yang model cm handle retriever is not invoked' + 0 * mockYangModelCmHandleRetriever.getDmiServiceNamesAndProperties(_) + } + def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() { given: 'cpsDataService returns valid datanode' mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', @@ -340,12 +330,28 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { '{some-json}', 'application/json') then: 'an exception is thrown with the expected error message details with correct operation' - def exceptionThrown = thrown(ServerNcmpException.class) + def exceptionThrown = thrown(HttpClientRequestException.class) exceptionThrown.getMessage().contains(expectedResponseMessage) where: scenario | givenOperation || expectedResponseMessage - 'CREATE' | CREATE || 'Not able to create resource data.' - 'READ' | READ || 'Not able to read resource data.' - 'UPDATE' | UPDATE || 'Not able to update resource data.' + 'CREATE' | CREATE || 'Unable to create resource data.' + 'READ' | READ || 'Unable to read resource data.' + 'UPDATE' | UPDATE || 'Unable to update resource data.' + } + + def 'Verify modules and create anchor params'() { + given: 'dmi plugin registration return created cm handles' + def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'service1', dmiModelPlugin: 'service1', + dmiDataPlugin: 'service2') + dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle] + mockDmiPluginRegistration.getCreatedCmHandles() >> [ncmpServiceCmHandle] + when: 'parse and create cm handle in dmi registration then sync module' + objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(mockDmiPluginRegistration) + then: 'validate params for creating anchor and list elements' + 1 * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry', + '/dmi-registry', '{"cm-handles":[{"id":"some-cm-handle-id",' + + '"additional-properties":[],"public-properties":[]}]}', null) + 1 * mockCpsAdminService.createAnchor('NFP-Operational', null, + 'some-cm-handle-id') } } 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 9b8d4ada56..5eba5eecd2 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 @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +21,17 @@ 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 + import org.onap.cps.api.CpsDataService 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 spock.lang.Specification @@ -50,7 +57,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification { given: 'the CPS service return a CM handle' mockCpsDataService.getDataNode(dataspaceName, anchorName, cmHandleXpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode and: 'an update cm handle request with public properties updates' - def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleID: cmHandleId, publicProperties: updatedPublicProperties)] + def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: updatedPublicProperties)] when: 'update data node leaves is called with the update request' objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) then: 'the replace list method is called with correct params' @@ -72,7 +79,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification { given: 'the CPS service return a CM handle' mockCpsDataService.getDataNode(dataspaceName, anchorName, cmHandleXpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode and: 'an update cm handle request with DMI properties updates' - def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleID: cmHandleId, dmiProperties: updatedDmiProperties)] + def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: updatedDmiProperties)] when: 'update data node leaves is called with the update request' objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) then: 'replace list method should is called with correct params' @@ -96,7 +103,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification { def cmHandleDataNode = new DataNode(xpath: cmHandleXpath, childDataNodes: originalPropertyDataNodes) mockCpsDataService.getDataNode(dataspaceName, anchorName, cmHandleXpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode and: 'an update cm handle request that removes all public properties(existing and non-existing)' - def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleID: cmHandleId, publicProperties: ['publicProp3': null, 'publicProp4': null])] + def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp3': null, 'publicProp4': null])] when: 'update data node leaves is called with the update request' objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) then: 'the replace list method is not called' @@ -113,16 +120,58 @@ 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: [:])] + def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: [:], dmiProperties: [:])] and: 'data node cannot be found' - mockCpsDataService.getDataNode(*_) >> { throw new DataNodeNotFoundException(dataspaceName, anchorName, cmHandleXpath) } + mockCpsDataService.getDataNode(*_) >> { throw exception } when: 'update data node leaves is called using correct parameters' - objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) - then: 'data validation exception is thrown' - def exceptionThrown = thrown(DataValidationException.class) - assert exceptionThrown.getMessage().contains('DataNode not found') + def response = objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) + then: 'one failed registration response' + response.size() == 1 + and: 'it has expected error details' + with(response.get(0)) { + assert it.status == Status.FAILURE + assert it.cmHandle == cmHandleId + assert it.registrationError == expectedError + assert it.errorText == expectedErrorText + } + where: + 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'() { + given: 'cm handles request' + def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:]), + new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:]), + new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:])] + and: 'data node can be found for 1st and 3rd cm-handle but not for 2nd cm-handle' + mockCpsDataService.getDataNode(*_) >> cmHandleDataNode >> { throw new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry') } >> cmHandleDataNode + when: 'update data node leaves is called using correct parameters' + def cmHandleResponseList = objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) + then: 'response has 3 values' + cmHandleResponseList.size() == 3 + and: 'the 1st and 3rd requests were processed successfully' + with(cmHandleResponseList.get(0)) { + assert it.status == Status.SUCCESS + assert it.cmHandle == cmHandleId + } + with(cmHandleResponseList.get(2)) { + assert it.status == Status.SUCCESS + assert it.cmHandle == cmHandleId + } + and: 'the 2nd request failed with correct error code' + with(cmHandleResponseList.get(1)) { + assert it.status == Status.FAILURE + assert it.cmHandle == cmHandleId + assert it.registrationError == CM_HANDLE_DOES_NOT_EXIST + assert it.errorText == "cm-handle does not exist" + } + then: 'the replace list method is called twice' + 2 * mockCpsDataService.replaceListContent(*_) } def convertToProperties(expectedPropertiesAfterUpdateAsMap) { @@ -133,4 +182,5 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification { })) return properties } + } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy index 389086c770..394df1d076 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,22 +44,12 @@ class DmiRestClientSpec extends Specification { DmiRestClient objectUnderTest def resourceUrl = 'some url' - def 'DMI POST operation'() { - given: 'the rest template returns a valid response entity' - def mockResponseEntity = Mock(ResponseEntity) - mockRestTemplate.exchange(resourceUrl, HttpMethod.POST, _ as HttpEntity, Object.class) >> mockResponseEntity - when: 'POST operation is invoked' - def result = objectUnderTest.postOperation(resourceUrl, new HttpHeaders()) - then: 'the output of the method is equal to the output from the rest template' - result == mockResponseEntity - } - def 'DMI POST operation with JSON.'() { given: 'the rest template returns a valid response entity' def mockResponseEntity = Mock(ResponseEntity) mockRestTemplate.postForEntity(resourceUrl, _ as HttpEntity, Object.class) >> mockResponseEntity when: 'POST operation is invoked' - def result = objectUnderTest.postOperationWithJsonData(resourceUrl, 'json-data', new HttpHeaders()) + def result = objectUnderTest.postOperationWithJsonData(resourceUrl, 'json-data') then: 'the output of the method is equal to the output from the test template' result == mockResponseEntity } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy index 3df862ac5c..2a19df1723 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021-2022 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,12 +63,11 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { and: 'a positive response from DMI service when it is called with the expected parameters' def responseFromDmi = new ResponseEntity<Object>(HttpStatus.OK) def expectedUrl = dmiServiceBaseUrl + "${expectedDatastoreInUrl}?resourceIdentifier=${resourceIdentifier}${expectedOptionsInUrl}" - mockDmiRestClient.postOperationWithJsonData(expectedUrl, - expectedJson, [Accept: ['sample accept header']]) >> responseFromDmi + mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson) >> responseFromDmi dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl when: 'get resource data is invoked' def result = objectUnderTest.getResourceDataFromDmi(cmHandleId, resourceIdentifier, - options, 'sample accept header', dataStore, NO_REQUEST_ID, NO_TOPIC) + options, dataStore, NO_REQUEST_ID, NO_TOPIC) then: 'the result is the response from the DMI service' assert result == responseFromDmi where: 'the following parameters are used' @@ -88,7 +88,7 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { def expectedJson = '{"operation":"' + expectedOperationInUrl + '","dataType":"some data type","data":"requestData","cmHandleProperties":{"prop1":"val1"}}' def responseFromDmi = new ResponseEntity<Object>(HttpStatus.OK) dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl - mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson, [:]) >> responseFromDmi + mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson) >> responseFromDmi when: 'write resource method is invoked' def result = objectUnderTest.writeResourceDataPassThroughRunningFromDmi(cmHandleId, 'parent/child', operation, 'requestData', 'some data type') then: 'the result is the response from the DMI service' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy index d3fc17cc07..574f609e9e 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021-2022 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +56,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { def moduleReferencesAsLisOfMaps = [[moduleName: 'mod1', revision: 'A'], [moduleName: 'mod2', revision: 'X']] def expectedUrl = "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/modules" def responseFromDmi = new ResponseEntity([schemas: moduleReferencesAsLisOfMaps], HttpStatus.OK) - mockDmiRestClient.postOperationWithJsonData(expectedUrl, '{"cmHandleProperties":{}}', [:]) + mockDmiRestClient.postOperationWithJsonData(expectedUrl, '{"cmHandleProperties":{}}') >> responseFromDmi when: 'get module references is called' def result = objectUnderTest.getModuleReferences(yangModelCmHandle) @@ -88,7 +89,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { and: 'a positive response from DMI service when it is called with tha expected parameters' def responseFromDmi = new ResponseEntity<String>(HttpStatus.OK) mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/modules", - '{"cmHandleProperties":' + expectedAdditionalPropertiesInRequest + '}', [:]) >> responseFromDmi + '{"cmHandleProperties":' + expectedAdditionalPropertiesInRequest + '}') >> responseFromDmi when: 'a get module references is called' def result = objectUnderTest.getModuleReferences(yangModelCmHandle) then: 'the result is the response from DMI service' @@ -107,7 +108,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { [moduleName: 'mod2', revision: 'C', yangSource: 'other yang source']], HttpStatus.OK) def expectedModuleReferencesInRequest = '{"name":"mod1","revision":"A"},{"name":"mod2","revision":"X"}' mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/moduleResources", - '{"data":{"modules":[' + expectedModuleReferencesInRequest + ']},"cmHandleProperties":{}}', [:]) >> responseFromDmi + '{"data":{"modules":[' + expectedModuleReferencesInRequest + ']},"cmHandleProperties":{}}') >> responseFromDmi when: 'get new yang resources from DMI service' def result = objectUnderTest.getNewYangResourcesFromDmi(yangModelCmHandle, newModuleReferences) then: 'the result has the 2 expected yang (re)sources (order is not guaranteed)' @@ -139,8 +140,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { and: 'a positive response from DMI service when it is called with the expected parameters' def responseFromDmi = new ResponseEntity<>([[moduleName: 'mod1', revision: 'A', yangSource: 'some yang source']], HttpStatus.OK) mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/moduleResources", - '{"data":{"modules":[' + expectedModuleReferencesInRequest + ']},"cmHandleProperties":'+expectedAdditionalPropertiesInRequest+'}', - [:]) >> responseFromDmi + '{"data":{"modules":[' + expectedModuleReferencesInRequest + ']},"cmHandleProperties":'+expectedAdditionalPropertiesInRequest+'}') >> responseFromDmi when: 'get new yang resources from DMI service' def result = objectUnderTest.getNewYangResourcesFromDmi(yangModelCmHandle, unknownModuleReferences) then: 'the result is the response from DMI service' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy index e6f63ce1a2..563116f402 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy @@ -48,7 +48,7 @@ abstract class DmiOperationsBaseSpec extends Specification { def yangModelCmHandle = new YangModelCmHandle() def static dmiServiceName = 'some service name' - def static cmHandleId = 'some cm handle' + def static cmHandleId = 'some-cm-handle' def static resourceIdentifier = 'parent/child' def mockYangModelCmHandleRetrieval(dmiProperties) { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetrieverSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetrieverSpec.groovy index 593a6ec936..bc30c9c777 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetrieverSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetrieverSpec.groovy @@ -22,6 +22,7 @@ package org.onap.cps.ncmp.api.impl.operations import org.onap.cps.api.CpsDataService import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import org.onap.cps.spi.exceptions.DataValidationException import spock.lang.Shared import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS @@ -34,19 +35,19 @@ class YangModelCmHandleRetrieverSpec extends Specification { def objectUnderTest = new YangModelCmHandleRetriever(mockCpsDataService) - def cmHandleId = 'some cm handle' + def cmHandleId = 'some-cm-handle' def leaves = ["dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"] - def xpath = "/dmi-registry/cm-handles[@id='some cm handle']" + def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']" @Shared def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]), new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])] @Shared - def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])] + def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])] @Shared - def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])] + def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])] def "Retrieve CmHandle using datanode with #scenario."() { given: 'the cps data service returns a data node from the DMI registry' @@ -69,4 +70,13 @@ class YangModelCmHandleRetrieverSpec extends Specification { 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] } + + def "Retrieve CmHandle using datanode with invalid CmHandle id."() { + when: 'retrieving the yang modelled cm handle with an invalid id' + def result = objectUnderTest.getDmiServiceNamesAndProperties('cm handle id with spaces') + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the result is not returned' + result == null + } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplModelSyncSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncServiceSpec.groovy index 553ac72790..37fdbeeb2a 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplModelSyncSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncServiceSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2022 Nordix Foundation + * 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. @@ -18,32 +18,22 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api.impl +package org.onap.cps.ncmp.api.inventory.sync -import org.onap.cps.api.CpsAdminService import org.onap.cps.api.CpsModuleService -import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations -import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle import org.onap.cps.spi.model.ModuleReference -import org.onap.cps.utils.JsonObjectMapper import spock.lang.Specification -class NetworkCmProxyDataServiceImplModelSyncSpec extends Specification { +class ModuleSyncServiceSpec extends Specification { + - def nullCpsDataService = null - def mockJsonObjectMapper = Mock(JsonObjectMapper) def mockCpsModuleService = Mock(CpsModuleService) - def mockCpsAdminService = Mock(CpsAdminService) def mockDmiModelOperations = Mock(DmiModelOperations) - def mockDmiDataOperations = Mock(DmiDataOperations) - def mockYangModelCmHandleRetriever = Mock(YangModelCmHandleRetriever) - def nullNetworkCmProxyDataServicePropertyHandler = null - def objectUnderTest = new NetworkCmProxyDataServiceImpl(nullCpsDataService, mockJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations, - mockCpsModuleService, mockCpsAdminService, nullNetworkCmProxyDataServicePropertyHandler,mockYangModelCmHandleRetriever) + def objectUnderTest = new ModuleSyncService(mockDmiModelOperations, mockCpsModuleService) def expectedDataspaceName = 'NFP-Operational' @@ -51,7 +41,7 @@ class NetworkCmProxyDataServiceImplModelSyncSpec extends Specification { given: 'a cm handle' def ncmpServiceCmHandle = new NcmpServiceCmHandle() def dmiServiceName = 'some service name' - ncmpServiceCmHandle.cmHandleID = 'cm handle id 1' + ncmpServiceCmHandle.cmHandleId = 'cmHandleId-1' def yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle(dmiServiceName, '' , '', ncmpServiceCmHandle) and: 'DMI operations returns some module references' def moduleReferences = [ new ModuleReference(moduleName:'module1',revision:'1'), @@ -63,16 +53,14 @@ class NetworkCmProxyDataServiceImplModelSyncSpec extends Specification { mockDmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle, [new ModuleReference('module1', '1')]) >> yangResourceToContentMap when: 'module sync is triggered' mockCpsModuleService.identifyNewModuleReferences(moduleReferences) >> toModuleReference(identifiedNewModuleReferences) - objectUnderTest.syncModulesAndCreateAnchor(yangModelCmHandle) - then: 'the CPS module service is called once with the correct parameters' - 1 * mockCpsModuleService.createSchemaSetFromModules(expectedDataspaceName, yangModelCmHandle.getId(), yangResourceToContentMap, toModuleReference(expectedKnownModules)) - and: 'admin service create anchor method has been called with correct parameters' - 1 * mockCpsAdminService.createAnchor(expectedDataspaceName, yangModelCmHandle.getId(), yangModelCmHandle.getId()) + def result = objectUnderTest.syncAndCreateSchemaSet(yangModelCmHandle) + then: 'the resulting schema set name is the same as the cm handle id' + assert result == 'cmHandleId-1' where: 'the following parameters are used' - scenario | existingModuleResourcesInCps | identifiedNewModuleReferences | yangResourceToContentMap || expectedKnownModules - 'one new module' | [['module2' : '2'], ['module3' : '3']] | [['module1' : '1']] | [module1: 'some yang source'] || [['module2' : '2']] - 'no add. properties' | [['module2' : '2'], ['module3' : '3']] | [['module1' : '1']] | [module1: 'some yang source'] || [['module2' : '2']] - 'no new module' | [['module1' : '1'], ['module2' : '2']] | [] | [:] || [['module1' : '1'], ['module2' : '2']] + scenario | existingModuleResourcesInCps | identifiedNewModuleReferences | yangResourceToContentMap + 'one new module' | [['module2' : '2'], ['module3' : '3']] | [['module1' : '1']] | [module1: 'some yang source'] + 'no add. properties' | [['module2' : '2'], ['module3' : '3']] | [['module1' : '1']] | [module1: 'some yang source'] + 'no new module' | [['module1' : '1'], ['module2' : '2']] | [] | [:] } def toModuleReference(moduleReferenceAsMap) { 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 new file mode 100644 index 0000000000..4476998d82 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy @@ -0,0 +1,71 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Bell Canada + * ================================================================================ + * 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.models + +import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError +import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status +import spock.lang.Specification + +class CmHandleRegistrationResponseSpec extends Specification { + + 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) { + assert it.cmHandle == 'cmHandle' + assert it.status == Status.SUCCESS + } + and: 'error details are null' + cmHandleRegistrationResponse.registrationError == null + cmHandleRegistrationResponse.errorText == null + } + + 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' + with(cmHandleRegistrationResponse) { + assert it.registrationError == RegistrationError.UNKNOWN_ERROR + assert it.cmHandle == 'cmHandle' + assert errorText == 'unexpected error' + } + } + + def 'Failed cm-handle Registration Response: for #scenario'() { + when: 'cm-handle failure response is created for #scenario' + def cmHandleRegistrationResponse = + CmHandleRegistrationResponse.createFailureResponse(cmHandleId, registrationError) + then: 'the response is created with expected value' + with(cmHandleRegistrationResponse) { + assert it.registrationError == registrationError + assert it.cmHandle == cmHandleId + assert it.status == Status.FAILURE + 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-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/YangModelCmHandleSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/YangModelCmHandleSpec.groovy index 470015ec17..7bbc3d7533 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/YangModelCmHandleSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/YangModelCmHandleSpec.groovy @@ -31,6 +31,7 @@ class YangModelCmHandleSpec extends Specification { def 'Creating yang model cm handle from a service api cm handle.'() { given: 'a cm handle with properties' def ncmpServiceCmHandle = new NcmpServiceCmHandle() + ncmpServiceCmHandle.cmHandleId = 'cm-handle-id01' ncmpServiceCmHandle.dmiProperties = [myDmiProperty:'value1'] ncmpServiceCmHandle.publicProperties = [myPublicProperty:'value2'] when: 'it is converted to a yang model cm handle' @@ -47,7 +48,7 @@ class YangModelCmHandleSpec extends Specification { def 'Resolve DMI service name: #scenario and #requiredService service require.'() { given: 'a yang model cm handle' - def objectUnderTest = YangModelCmHandle.toYangModelCmHandle(dmiServiceName, dmiDataServiceName, dmiModelServiceName, new NcmpServiceCmHandle()) + def objectUnderTest = YangModelCmHandle.toYangModelCmHandle(dmiServiceName, dmiDataServiceName, dmiModelServiceName, new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1')) expect: assert objectUnderTest.resolveDmiServiceName(requiredService) == expectedService where: diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy index 1615d055db..4c8dcace7d 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy @@ -32,21 +32,21 @@ import spock.lang.Specification class DmiServiceUrlBuilderSpec extends Specification { @Shared - YangModelCmHandle yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle("dmiServiceName", - "dmiDataServiceName", "dmiModuleServiceName", new NcmpServiceCmHandle()) + YangModelCmHandle yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle('dmiServiceName', + 'dmiDataServiceName', 'dmiModuleServiceName', new NcmpServiceCmHandle(cmHandleId: 'some-cm-handle-id')) - NcmpConfiguration.DmiProperties dmiProperties = new NcmpConfiguration.DmiProperties(); + NcmpConfiguration.DmiProperties dmiProperties = new NcmpConfiguration.DmiProperties() def objectUnderTest = new DmiServiceUrlBuilder(dmiProperties) def 'Create the dmi service url with #scenario.'() { given: 'uri variables' - dmiProperties.dmiBasePath = 'dmi'; + dmiProperties.dmiBasePath = 'dmi' def uriVars = objectUnderTest.populateUriVariables(yangModelCmHandle, - "cmHandle", PASSTHROUGH_RUNNING); + "cmHandle", PASSTHROUGH_RUNNING) and: 'query params' def uriQueries = objectUnderTest.populateQueryParams(resourceId, - 'optionsParamInQuery', topicParamInQuery); + 'optionsParamInQuery', topicParamInQuery) when: 'a dmi datastore service url is generated' def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars) then: 'service url is generated as expected' @@ -61,12 +61,12 @@ class DmiServiceUrlBuilderSpec extends Specification { def 'Populate dmi data store url #scenario.'() { given: 'uri variables are created' - dmiProperties.dmiBasePath = dmiBasePath; + dmiProperties.dmiBasePath = dmiBasePath def uriVars = objectUnderTest.populateUriVariables(yangModelCmHandle, - "cmHandle", PASSTHROUGH_RUNNING); + "cmHandle", PASSTHROUGH_RUNNING) and: 'null query params' def uriQueries = objectUnderTest.populateQueryParams(null, - null, null); + null, null) when: 'a dmi datastore service url is generated' def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars) then: 'the created dmi service url matches the expected' diff --git a/cps-parent/pom.xml b/cps-parent/pom.xml index e03dce3db2..b76c63c6f7 100755 --- a/cps-parent/pom.xml +++ b/cps-parent/pom.xml @@ -32,7 +32,7 @@ <groupId>org.onap.cps</groupId> <artifactId>cps-parent</artifactId> - <version>3.0.0-SNAPSHOT</version> + <version>3.1.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> @@ -115,7 +115,7 @@ <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> - <version>2.3.3.RELEASE</version> + <version>2.6.4</version> <executions> <execution> <goals> diff --git a/cps-path-parser/pom.xml b/cps-path-parser/pom.xml index c8b88e8aa0..1b0ebe2ef2 100644 --- a/cps-path-parser/pom.xml +++ b/cps-path-parser/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.onap.cps</groupId> <artifactId>cps-parent</artifactId> - <version>3.0.0-SNAPSHOT</version> + <version>3.1.0-SNAPSHOT</version> <relativePath>../cps-parent/pom.xml</relativePath> </parent> @@ -34,6 +34,7 @@ <plugin> <groupId>org.antlr</groupId> <artifactId>antlr4-maven-plugin</artifactId> + <version>4.9.2</version> <executions> <execution> <goals> diff --git a/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 b/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 index cefeac4387..40ad410a0d 100644 --- a/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 +++ b/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Nordix Foundation + * 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. @@ -20,7 +20,7 @@ grammar CpsPath ; -cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? ancestorAxis? ; +cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? ancestorAxis? invalidPostFix?; ancestorAxis : SLASH KW_ANCESTOR COLONCOLON ancestorPath ; @@ -46,6 +46,8 @@ leafCondition : AT leafName EQ ( IntegerLiteral | StringLiteral) ; leafName : QName ; +invalidPostFix : (AT | CB | COLONCOLON | EQ ).+ ; + /* * Lexer Rules * Most of the lexer rules below are inspired by diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java index ebf6fd3c91..21f5173a98 100644 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Nordix Foundation + * 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. @@ -25,6 +25,7 @@ import static org.onap.cps.cpspath.parser.CpsPathPrefixType.DESCENDANT; import java.util.HashMap; import java.util.Map; import org.onap.cps.cpspath.parser.antlr4.CpsPathBaseListener; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser; import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.AncestorAxisContext; import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.DescendantContext; import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.IncorrectPrefixContext; @@ -35,18 +36,33 @@ import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.TextFunctionConditionCon public class CpsPathBuilder extends CpsPathBaseListener { + private static final String OPEN_BRACKET = "["; + + private static final String CLOSE_BRACKET = "]"; + final CpsPathQuery cpsPathQuery = new CpsPathQuery(); final Map<String, Object> leavesData = new HashMap<>(); + final StringBuilder normalizedXpathBuilder = new StringBuilder(); + + final StringBuilder normalizedAncestorPathBuilder = new StringBuilder(); + + boolean processingAncestorAxis = false; + + @Override + public void exitInvalidPostFix(final CpsPathParser.InvalidPostFixContext ctx) { + throw new PathParsingException(ctx.getText()); + } + @Override public void exitPrefix(final PrefixContext ctx) { - cpsPathQuery.setXpathPrefix(ctx.getText()); + cpsPathQuery.setXpathPrefix(normalizedXpathBuilder.toString()); } @Override public void exitIncorrectPrefix(final IncorrectPrefixContext ctx) { - throw new IllegalStateException("CPS path can only start with one or two slashes (/)"); + throw new PathParsingException("CPS path can only start with one or two slashes (/)"); } @Override @@ -56,32 +72,49 @@ public class CpsPathBuilder extends CpsPathBaseListener { comparisonValue = Integer.valueOf(ctx.IntegerLiteral().getText()); } if (ctx.StringLiteral() != null) { + final boolean wasWrappedInDoubleQuote = ctx.StringLiteral().getText().startsWith("\""); comparisonValue = stripFirstAndLastCharacter(ctx.StringLiteral().getText()); + if (wasWrappedInDoubleQuote) { + comparisonValue = String.valueOf(comparisonValue).replace("'", "\\'"); + } } else if (comparisonValue == null) { - throw new IllegalStateException("Unsupported comparison value encountered in expression" + ctx.getText()); + throw new PathParsingException("Unsupported comparison value encountered in expression" + ctx.getText()); } leavesData.put(ctx.leafName().getText(), comparisonValue); + appendCondition(normalizedXpathBuilder, ctx.leafName().getText(), comparisonValue); + if (processingAncestorAxis) { + appendCondition(normalizedAncestorPathBuilder, ctx.leafName().getText(), comparisonValue); + } } @Override public void exitDescendant(final DescendantContext ctx) { cpsPathQuery.setCpsPathPrefixType(DESCENDANT); - cpsPathQuery.setDescendantName(ctx.getText().substring(2)); + cpsPathQuery.setDescendantName(normalizedXpathBuilder.substring(1)); + normalizedXpathBuilder.insert(0, "/"); } @Override public void enterMultipleLeafConditions(final MultipleLeafConditionsContext ctx) { + normalizedXpathBuilder.append(OPEN_BRACKET); leavesData.clear(); } @Override public void exitMultipleLeafConditions(final MultipleLeafConditionsContext ctx) { + normalizedXpathBuilder.append(CLOSE_BRACKET); cpsPathQuery.setLeavesData(leavesData); } @Override + public void enterAncestorAxis(final AncestorAxisContext ctx) { + processingAncestorAxis = true; + } + + @Override public void exitAncestorAxis(final AncestorAxisContext ctx) { - cpsPathQuery.setAncestorSchemaNodeIdentifier(ctx.ancestorPath().getText()); + cpsPathQuery.setAncestorSchemaNodeIdentifier(normalizedAncestorPathBuilder.substring(1)); + processingAncestorAxis = false; } @Override @@ -90,7 +123,24 @@ public class CpsPathBuilder extends CpsPathBaseListener { cpsPathQuery.setTextFunctionConditionValue(stripFirstAndLastCharacter(ctx.StringLiteral().getText())); } + @Override + public void enterListElementRef(final CpsPathParser.ListElementRefContext ctx) { + normalizedXpathBuilder.append(OPEN_BRACKET); + if (processingAncestorAxis) { + normalizedAncestorPathBuilder.append(OPEN_BRACKET); + } + } + + @Override + public void exitListElementRef(final CpsPathParser.ListElementRefContext ctx) { + normalizedXpathBuilder.append(CLOSE_BRACKET); + if (processingAncestorAxis) { + normalizedAncestorPathBuilder.append(CLOSE_BRACKET); + } + } + CpsPathQuery build() { + cpsPathQuery.setNormalizedXpath(normalizedXpathBuilder.toString()); return cpsPathQuery; } @@ -98,4 +148,23 @@ public class CpsPathBuilder extends CpsPathBaseListener { return wrappedString.substring(1, wrappedString.length() - 1); } + @Override + public void exitContainerName(final CpsPathParser.ContainerNameContext ctx) { + normalizedXpathBuilder.append("/") + .append(ctx.getText()); + if (processingAncestorAxis) { + normalizedAncestorPathBuilder.append("/").append(ctx.getText()); + } + } + + private void appendCondition(final StringBuilder currentNormalizedPathBuilder, final String name, + final Object value) { + final char lastCharacter = currentNormalizedPathBuilder.charAt(currentNormalizedPathBuilder.length() - 1); + currentNormalizedPathBuilder.append(lastCharacter == '[' ? "" : " and ") + .append("@") + .append(name) + .append("='") + .append(value) + .append("'"); + } } diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java index de7adf2b71..53490f3a2d 100644 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Nordix Foundation + * 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. @@ -26,19 +26,13 @@ import java.util.Map; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; -import org.antlr.v4.runtime.BaseErrorListener; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.RecognitionException; -import org.antlr.v4.runtime.Recognizer; -import org.onap.cps.cpspath.parser.antlr4.CpsPathLexer; -import org.onap.cps.cpspath.parser.antlr4.CpsPathParser; @Getter @Setter(AccessLevel.PACKAGE) public class CpsPathQuery { private String xpathPrefix; + private String normalizedXpath; private CpsPathPrefixType cpsPathPrefixType = ABSOLUTE; private String descendantName; private Map<String, Object> leavesData; @@ -53,20 +47,7 @@ public class CpsPathQuery { * @return a CpsPathQuery object. */ public static CpsPathQuery createFrom(final String cpsPathSource) { - final var inputStream = CharStreams.fromString(cpsPathSource); - final var cpsPathLexer = new CpsPathLexer(inputStream); - final var cpsPathParser = new CpsPathParser(new CommonTokenStream(cpsPathLexer)); - cpsPathParser.addErrorListener(new BaseErrorListener() { - @Override - public void syntaxError(final Recognizer<?, ?> recognizer, final Object offendingSymbol, final int line, - final int charPositionInLine, final String msg, final RecognitionException e) { - throw new IllegalStateException("failed to parse at line " + line + " due to " + msg, e); - } - }); - final var cpsPathBuilder = new CpsPathBuilder(); - cpsPathParser.addParseListener(cpsPathBuilder); - cpsPathParser.cpsPath(); - return cpsPathBuilder.build(); + return CpsPathUtil.getCpsPathQuery(cpsPathSource); } /** diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java new file mode 100644 index 0000000000..97d7d1d760 --- /dev/null +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java @@ -0,0 +1,81 @@ +/* + * ============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.cpspath.parser; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.onap.cps.cpspath.parser.antlr4.CpsPathLexer; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser; + +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PACKAGE) +public class CpsPathUtil { + + /** + * Returns a normalized xpath path query. + * + * @param xpathSource xpath + * @return a normalized xpath String. + */ + public static String getNormalizedXpath(final String xpathSource) { + final CpsPathBuilder cpsPathBuilder = getCpsPathBuilder(xpathSource); + return cpsPathBuilder.build().getNormalizedXpath(); + } + + /** + * Returns a cps path query. + * + * @param cpsPathSource cps path + * @return a CpsPathQuery object. + */ + + public static CpsPathQuery getCpsPathQuery(final String cpsPathSource) { + final CpsPathBuilder cpsPathBuilder = getCpsPathBuilder(cpsPathSource); + return cpsPathBuilder.build(); + } + + private static CpsPathBuilder getCpsPathBuilder(final String cpsPathSource) { + final CharStream inputStream = CharStreams.fromString(cpsPathSource); + final CpsPathLexer cpsPathLexer = new CpsPathLexer(inputStream); + final CpsPathParser cpsPathParser = new CpsPathParser(new CommonTokenStream(cpsPathLexer)); + cpsPathParser.addErrorListener(new BaseErrorListener() { + @Override + public void syntaxError(final Recognizer<?, ?> recognizer, final Object offendingSymbol, final int line, + final int charPositionInLine, final String msg, final RecognitionException e) { + throw new PathParsingException("failed to parse at line " + line + " due to " + msg, + e == null ? "" : e.getMessage()); + } + }); + final CpsPathBuilder cpsPathBuilder = new CpsPathBuilder(); + cpsPathParser.addParseListener(cpsPathBuilder); + cpsPathParser.cpsPath(); + return cpsPathBuilder; + } +} diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java new file mode 100755 index 0000000000..4a67167c96 --- /dev/null +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java @@ -0,0 +1,55 @@ +/* + * ============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.cpspath.parser; + +import lombok.Getter; + +/** + * XPath Parsing Exception. + */ +public class PathParsingException extends RuntimeException { + + private static final long serialVersionUID = 7072864354925271894L; + + @Getter + final String details; + + /** + * Constructor. + * + * @param details the error details + */ + public PathParsingException(final String details) { + super("Error while parsing xpath expression"); + this.details = details; + } + + /** + * Constructor. + * + * @param message the error message + * @param details the error details + */ + public PathParsingException(final String message, final String details) { + super(message); + this.details = details; + } +} diff --git a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy index bfec574eba..b837a64fef 100644 --- a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy +++ b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Nordix Foundation + * 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. @@ -34,17 +34,17 @@ class CpsPathQuerySpec extends Specification { result.cpsPathPrefixType == ABSOLUTE and: 'the right query parameters are set' result.xpathPrefix == expectedXpathPrefix - result.hasLeafConditions() == true - result.leavesData.containsKey(expectedLeafName) == true + result.hasLeafConditions() + result.leavesData.containsKey(expectedLeafName) result.leavesData.get(expectedLeafName) == expectedLeafValue where: 'the following data is used' - scenario | cpsPath || expectedXpathPrefix | expectedLeafName | expectedLeafValue - 'leaf of type String' | '/parent/child[@common-leaf-name="common-leaf-value"]' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value' - 'leaf of type String' | '/parent/child[@common-leaf-name=\'common-leaf-value\']' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value' - 'leaf of type Integer' | '/parent/child[@common-leaf-name-int=5]' || '/parent/child' | 'common-leaf-name-int' | 5 - 'spaces around =' | '/parent/child[@common-leaf-name-int = 5]' || '/parent/child' | 'common-leaf-name-int' | 5 - 'key in top container' | '/parent[@common-leaf-name-int=5]' || '/parent' | 'common-leaf-name-int' | 5 - 'parent list' | '/shops/shop[@id=1]/categories[@id=1]/book[@title="Dune"]' || '/shops/shop[@id=1]/categories[@id=1]/book' | 'title' | 'Dune' + scenario | cpsPath || expectedXpathPrefix | expectedLeafName | expectedLeafValue + 'leaf of type String' | '/parent/child[@common-leaf-name="common-leaf-value"]' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value' + 'leaf of type String' | '/parent/child[@common-leaf-name=\'common-leaf-value\']' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value' + 'leaf of type Integer' | '/parent/child[@common-leaf-name-int=5]' || '/parent/child' | 'common-leaf-name-int' | 5 + 'spaces around =' | '/parent/child[@common-leaf-name-int = 5]' || '/parent/child' | 'common-leaf-name-int' | 5 + 'key in top container' | '/parent[@common-leaf-name-int=5]' || '/parent' | 'common-leaf-name-int' | 5 + 'parent list' | '/shops/shop[@id=1]/categories[@id=1]/book[@title="Dune"]' || "/shops/shop[@id='1']/categories[@id='1']/book" | 'title' | 'Dune' } def 'Parse cps path of type ends with a #scenario.'() { @@ -60,6 +60,38 @@ class CpsPathQuerySpec extends Specification { 'parent & child' | '//parent/child' || 'parent/child' } + def 'Parse cps path to form the Normalized cps path containing #scenario.'() { + when: 'the given cps path is parsed' + def result = CpsPathUtil.getCpsPathQuery(cpsPath) + then: 'the query has the right normalized xpath type' + assert result.normalizedXpath == expectedNormalizedXPath + where: 'the following data is used' + scenario | cpsPath || expectedNormalizedXPath + 'yang container' | '/cps-path' || '/cps-path' + 'descendant anywhere' | '//cps-path' || '//cps-path' + 'descendant with leaf condition' | '//cps-path[@key=1]' || "//cps-path[@key='1']" + 'descendant with leaf value and ancestor' | '//cps-path[@key=1]/ancestor:parent[@key=1]' || "//cps-path[@key='1']/ancestor:parent[@key='1']" + 'parent & child' | '/parent/child' || '/parent/child' + 'parent leaf of type Integer & child' | '/parent/child[@code=1]/child2' || "/parent/child[@code='1']/child2" + 'parent leaf with double quotes' | '/parent/child[@code="1"]/child2' || "/parent/child[@code='1']/child2" + 'parent leaf with double quotes inside single quotes' | '/parent/child[@code=\'"1"\']/child2' || "/parent/child[@code='\"1\"']/child2" + 'parent leaf with single quotes inside double quotes' | '/parent/child[@code="\'1\'"]/child2' || "/parent/child[@code='\\\'1\\\'']/child2" + 'leaf with single quotes inside double quotes' | '/parent/child[@code="\'1\'"]' || "/parent/child[@code='\\\'1\\\'']" + 'leaf with more than one attribute' | '/parent/child[@key1=1 and @key2="abc"]' || "/parent/child[@key1='1' and @key2='abc']" + 'parent & child with more than one attribute' | '/parent/child[@key1=1 and @key2="abc"]/child2' || "/parent/child[@key1='1' and @key2='abc']/child2" + } + + def 'Parse xpath to form the Normalized xpath containing #scenario.'() { + when: 'the given xpath is parsed' + def result = CpsPathUtil.getNormalizedXpath(xPath) + then: 'the query has the right normalized xpath type' + assert result == expectedNormalizedXPath + where: 'the following data is used' + scenario | xPath || expectedNormalizedXPath + 'yang container' | '/xpath' || '/xpath' + 'descendant anywhere' | '//xpath' || '//xpath' + } + def 'Parse cps path that ends with a yang list containing #scenario.'() { when: 'the given cps path is parsed' def result = CpsPathQuery.createFrom(cpsPath) @@ -99,7 +131,7 @@ class CpsPathQuerySpec extends Specification { when: 'the given cps path is parsed' CpsPathQuery.createFrom(cpsPath) then: 'a CpsPathException is thrown' - thrown(IllegalStateException) + thrown(PathParsingException) where: 'the following data is used' scenario | cpsPath 'no / at the start' | 'invalid-cps-path/child' @@ -110,7 +142,9 @@ class CpsPathQuerySpec extends Specification { 'end with descendant and more than one attribute separated by "or"' | '//child[@int-leaf=5 or @leaf-name="leaf value"]' 'missing attribute value' | '//child[@int-leaf=5 and @name]' 'incomplete ancestor value' | '//books/ancestor::' -// DISCUSS WITH TEAM : 'unsupported postfix after value condition (JIRA CPS-450)' | '/parent/child[@id=1]/somePostFix' + 'invalid list element with missing [' | '/parent-206/child-206/grand-child-206@key="A"]' + 'invalid list element with incorrect ]' | '/parent-206/child-206/grand-child-206]@key="A"]' + 'invalid list element with incorrect ::' | '/parent-206/child-206/grand-child-206::@key"A"]' } def 'Parse cps path using ancestor by schema node identifier with a #scenario.'() { @@ -125,11 +159,12 @@ class CpsPathQuerySpec extends Specification { and: 'there are no leaves conditions' result.hasLeafConditions() == false where: - scenario | ancestorPath - 'basic container' | 'someContainer' - 'container with parent' | 'parent/child' - 'ancestor that is a list' | 'categories[@code=1]' - 'parent that is a list' | 'parent[@id=1]/child' + scenario | ancestorPath + 'basic container' | 'someContainer' + 'container with parent' | 'parent/child' + 'ancestor that is a list' | "categories[@code='1']" + 'ancestor that is a list with compound key' | "categories[@key1='1' and @key2='2']" + 'parent that is a list' | "parent[@id='1']/child" } def 'Combinations #scenario.'() { @@ -145,11 +180,10 @@ class CpsPathQuerySpec extends Specification { result.ancestorSchemaNodeIdentifier == 'someAncestor' result.descendantName == expectedDescendantName where: - scenario | cpsPath || expectedDescendantName | expectLeafConditions - 'basic container' | '//someContainer' || 'someContainer' | false - 'container with parent' | '//parent/child' || 'parent/child' | false - 'container with list-parent' | '//parent[@id=1]/child' || 'parent[@id=1]/child' | false - 'container with list-parent' | '//parent[@id=1]/child[@name="test"]' || 'parent[@id=1]/child' | true + scenario | cpsPath || expectedDescendantName | expectLeafConditions + 'basic container' | '//someContainer' || 'someContainer' | false + 'container with parent' | '//parent/child' || 'parent/child' | false + 'container with list-parent' | '//parent[@id=1]/child' || "parent[@id='1']/child" | false + 'container with list-parent' | '//parent[@id=1]/child[@name="test"]' || "parent[@id='1']/child" | true } - } 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/pom.xml b/cps-rest/pom.xml index 5a21957cd4..6019197269 100755 --- a/cps-rest/pom.xml +++ b/cps-rest/pom.xml @@ -28,7 +28,7 @@ <parent> <groupId>org.onap.cps</groupId> <artifactId>cps-parent</artifactId> - <version>3.0.0-SNAPSHOT</version> + <version>3.1.0-SNAPSHOT</version> <relativePath>../cps-parent/pom.xml</relativePath> </parent> 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-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy index 2aa4ddd1e5..d4c68c30a8 100644 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy @@ -24,7 +24,6 @@ package org.onap.cps.rest.exceptions import com.fasterxml.jackson.databind.ObjectMapper import groovy.json.JsonSlurper -import org.mapstruct.factory.Mappers import org.onap.cps.api.CpsAdminService import org.onap.cps.api.CpsDataService import org.onap.cps.api.CpsModuleService diff --git a/cps-ri/pom.xml b/cps-ri/pom.xml index 6e92894fca..98a392a5c2 100644 --- a/cps-ri/pom.xml +++ b/cps-ri/pom.xml @@ -26,7 +26,7 @@ <parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java index 50b27207ee..2e7bb7e969 100755 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java @@ -24,6 +24,7 @@ package org.onap.cps.spi.impl; import java.util.Collection; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import javax.transaction.Transactional; import lombok.AllArgsConstructor; @@ -36,8 +37,10 @@ import org.onap.cps.spi.exceptions.AlreadyDefinedException; import org.onap.cps.spi.exceptions.DataspaceInUseException; import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException; import org.onap.cps.spi.model.Anchor; +import org.onap.cps.spi.model.CmHandleQueryParameters; import org.onap.cps.spi.repository.AnchorRepository; import org.onap.cps.spi.repository.DataspaceRepository; +import org.onap.cps.spi.repository.ModuleReferenceRepository; import org.onap.cps.spi.repository.SchemaSetRepository; import org.onap.cps.spi.repository.YangResourceRepository; import org.springframework.dao.DataIntegrityViolationException; @@ -51,6 +54,7 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic private final AnchorRepository anchorRepository; private final SchemaSetRepository schemaSetRepository; private final YangResourceRepository yangResourceRepository; + private final ModuleReferenceRepository moduleReferenceRepository; @Override public void createDataspace(final String dataspaceName) { @@ -132,6 +136,11 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic anchorRepository.delete(anchorEntity); } + @Override + public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) { + return moduleReferenceRepository.queryCmHandles(cmHandleQueryParameters); + } + private AnchorEntity getAnchorEntity(final String dataspaceName, final String anchorName) { final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName); return anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java index 78862d7233..daf4dd757b 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java @@ -41,6 +41,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.StaleStateException; import org.onap.cps.cpspath.parser.CpsPathQuery; +import org.onap.cps.cpspath.parser.CpsPathUtil; +import org.onap.cps.cpspath.parser.PathParsingException; import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.entities.AnchorEntity; @@ -56,6 +58,7 @@ import org.onap.cps.spi.model.DataNodeBuilder; import org.onap.cps.spi.repository.AnchorRepository; import org.onap.cps.spi.repository.DataspaceRepository; import org.onap.cps.spi.repository.FragmentRepository; +import org.onap.cps.spi.utils.SessionManager; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; @@ -73,6 +76,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService private final JsonObjectMapper jsonObjectMapper; + private final SessionManager sessionManager; + private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})"; private static final Pattern REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE = Pattern.compile("\\[(\\@([^\\/]{0,9999}))\\]$"); @@ -171,8 +176,14 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService if (isRootXpath(xpath)) { return fragmentRepository.findFirstRootByDataspaceAndAnchor(dataspaceEntity, anchorEntity); } else { + final String normalizedXpath; + try { + normalizedXpath = CpsPathUtil.getNormalizedXpath(xpath); + } catch (final PathParsingException e) { + throw new CpsPathException(e.getMessage()); + } return fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, - xpath); + normalizedXpath); } } @@ -183,8 +194,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); final CpsPathQuery cpsPathQuery; try { - cpsPathQuery = CpsPathQuery.createFrom(cpsPath); - } catch (final IllegalStateException e) { + cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath); + } catch (final PathParsingException e) { throw new CpsPathException(e.getMessage()); } List<FragmentEntity> fragmentEntities = @@ -199,6 +210,22 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService .collect(Collectors.toUnmodifiableList()); } + @Override + public String startSession() { + return sessionManager.startSession(); + } + + @Override + public void closeSession(final String sessionId) { + sessionManager.closeSession(sessionId); + } + + @Override + public void lockAnchor(final String sessionId, final String dataspaceName, + final String anchorName, final Long timeoutInMilliseconds) { + sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds); + } + private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities, final CpsPathQuery cpsPathQuery) { final Set<String> ancestorXpath = new HashSet<>(); @@ -365,12 +392,13 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) { - if (parentFragmentEntity.getXpath().equals(targetXpath)) { + final String normalizedTargetXpath = CpsPathUtil.getNormalizedXpath(targetXpath); + if (parentFragmentEntity.getXpath().equals(normalizedTargetXpath)) { fragmentRepository.delete(parentFragmentEntity); return true; } if (parentFragmentEntity.getChildFragments() - .removeIf(fragment -> fragment.getXpath().equals(targetXpath))) { + .removeIf(fragment -> fragment.getXpath().equals(normalizedTargetXpath))) { fragmentRepository.save(parentFragmentEntity); return true; } @@ -378,7 +406,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) { - final String deleteTargetXpathPrefix = listXpath + "["; + final String normalizedListXpath = CpsPathUtil.getNormalizedXpath(listXpath); + final String deleteTargetXpathPrefix = normalizedListXpath + "["; if (parentFragmentEntity.getChildFragments() .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) { fragmentRepository.save(parentFragmentEntity); diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java index 6551937e10..4bc9dd9603 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java @@ -21,6 +21,8 @@ package org.onap.cps.spi.repository; import java.util.Collection; +import java.util.Set; +import org.onap.cps.spi.model.CmHandleQueryParameters; import org.onap.cps.spi.model.ModuleReference; /** @@ -31,4 +33,12 @@ public interface ModuleReferenceQuery { Collection<ModuleReference> identifyNewModuleReferences( final Collection<ModuleReference> moduleReferencesToCheck); + /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryParameters the cm handle query parameters + * @return collection of cm handle ids + */ + Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters); + } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java index ce2bfe7847..f70e218373 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java @@ -27,8 +27,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository -public interface ModuleReferenceRepository extends - JpaRepository<YangResourceEntity, Long>, ModuleReferenceQuery { +public interface ModuleReferenceRepository extends JpaRepository<YangResourceEntity, Long>, ModuleReferenceQuery { Collection<ModuleReference> identifyNewModuleReferences( final Collection<ModuleReference> moduleReferencesToCheck); diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java index 0e79deb8e8..f85dea3a73 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java @@ -24,21 +24,32 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; +import lombok.AllArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.onap.cps.spi.CpsDataPersistenceService; +import org.onap.cps.spi.FetchDescendantsOption; +import org.onap.cps.spi.model.CmHandleQueryParameters; +import org.onap.cps.spi.model.DataNode; import org.onap.cps.spi.model.ModuleReference; import org.springframework.transaction.annotation.Transactional; @Slf4j @Transactional +@AllArgsConstructor public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery { @PersistenceContext private EntityManager entityManager; + private final CpsDataPersistenceService cpsDataPersistenceService; + @Override @SneakyThrows public Collection<ModuleReference> identifyNewModuleReferences( @@ -57,6 +68,56 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery { return identifyNewModuleReferencesForCmHandle(tempTableName); } + /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryParameters the cm handle query parameters + * @return collection of cm handle ids + */ + @Override + public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) { + + if (cmHandleQueryParameters.getPublicProperties().entrySet().isEmpty()) { + return getAllCmHandles(); + } + + final Collection<DataNode> amalgamatedQueryResult = new ArrayList<>(); + int queryConditionCounter = 0; + for (final Map.Entry<String, String> entry : cmHandleQueryParameters.getPublicProperties().entrySet()) { + final StringBuilder cmHandlePath = new StringBuilder(); + cmHandlePath.append("//public-properties[@name='").append(entry.getKey()).append("' "); + cmHandlePath.append("and @value='").append(entry.getValue()).append("']"); + cmHandlePath.append("/ancestor::cm-handles"); + + final Collection<DataNode> singleConditionQueryResult = + cpsDataPersistenceService.queryDataNodes("NCMP-Admin", + "ncmp-dmi-registry", String.valueOf(cmHandlePath), FetchDescendantsOption.OMIT_DESCENDANTS); + if (++queryConditionCounter == 1) { + amalgamatedQueryResult.addAll(singleConditionQueryResult); + } else { + amalgamatedQueryResult.retainAll(singleConditionQueryResult); + } + + if (amalgamatedQueryResult.isEmpty()) { + break; + } + } + + return extractCmHandleIds(amalgamatedQueryResult); + } + + private Set<String> getAllCmHandles() { + final Collection<DataNode> cmHandles = cpsDataPersistenceService.queryDataNodes("NCMP-Admin", + "ncmp-dmi-registry", "//public-properties/ancestor::cm-handles", + FetchDescendantsOption.OMIT_DESCENDANTS); + return extractCmHandleIds(cmHandles); + } + + private Set<String> extractCmHandleIds(final Collection<DataNode> cmHandles) { + return cmHandles.stream().map(cmHandle -> cmHandle.getLeaves().get("id").toString()) + .collect(Collectors.toSet()); + } + private void createTemporaryTable(final String tempTableName) { final StringBuilder sqlStringBuilder = new StringBuilder("CREATE TEMPORARY TABLE " + tempTableName + "("); sqlStringBuilder.append(" id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,"); @@ -94,8 +155,8 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery { + " AND yang_resource.revision=%1$s.revision" + " WHERE yang_resource.module_name IS NULL;", tempTableName); - final List<Object[]> resultsAsObjects = - entityManager.createNativeQuery(sql).getResultList(); + @SuppressWarnings("unchecked") + final List<Object[]> resultsAsObjects = entityManager.createNativeQuery(sql).getResultList(); final List<ModuleReference> resultsAsModuleReferences = new ArrayList<>(resultsAsObjects.size()); for (final Object[] row : resultsAsObjects) { diff --git a/cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java b/cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java new file mode 100644 index 0000000000..e2786887ac --- /dev/null +++ b/cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java @@ -0,0 +1,165 @@ +/* + * ============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.spi.utils; + +import com.google.common.util.concurrent.TimeLimiter; +import com.google.common.util.concurrent.UncheckedExecutionException; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.HibernateException; +import org.hibernate.LockMode; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.Configuration; +import org.onap.cps.spi.entities.AnchorEntity; +import org.onap.cps.spi.entities.DataspaceEntity; +import org.onap.cps.spi.entities.SchemaSetEntity; +import org.onap.cps.spi.entities.YangResourceEntity; +import org.onap.cps.spi.exceptions.SessionManagerException; +import org.onap.cps.spi.exceptions.SessionTimeoutException; +import org.onap.cps.spi.repository.AnchorRepository; +import org.onap.cps.spi.repository.DataspaceRepository; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Slf4j +@Component +public class SessionManager { + + private final TimeLimiterProvider timeLimiterProvider; + private final DataspaceRepository dataspaceRepository; + private final AnchorRepository anchorRepository; + private static SessionFactory sessionFactory; + private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>(); + + private synchronized void buildSessionFactory() { + if (sessionFactory == null) { + sessionFactory = new Configuration().configure("hibernate.cfg.xml") + .addAnnotatedClass(AnchorEntity.class) + .addAnnotatedClass(DataspaceEntity.class) + .addAnnotatedClass(SchemaSetEntity.class) + .addAnnotatedClass(YangResourceEntity.class) + .buildSessionFactory(); + } + } + + /** + * Starts a session which allows use of locks and batch interaction with the persistence service. + * + * @return Session ID string + */ + public String startSession() { + buildSessionFactory(); + final Session session = sessionFactory.openSession(); + final String sessionId = UUID.randomUUID().toString(); + sessionMap.put(sessionId, session); + session.beginTransaction(); + return sessionId; + } + + /** + * Close session. + * Locks will be released and changes will be committed. + * + * @param sessionId session ID + */ + public void closeSession(final String sessionId) { + try { + final Session session = getSession(sessionId); + session.getTransaction().commit(); + session.close(); + } catch (final HibernateException e) { + throw new SessionManagerException("Cannot close session", + String.format("Unable to close session with session ID '%s'", sessionId), e); + } finally { + sessionMap.remove(sessionId); + } + } + + /** + * Lock Anchor. + * To release locks(s), the session holding the lock(s) must be closed. + * + * @param sessionId session ID + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param timeoutInMilliseconds lock attempt timeout in milliseconds + */ + @SneakyThrows + public void lockAnchor(final String sessionId, final String dataspaceName, + final String anchorName, final Long timeoutInMilliseconds) { + final ExecutorService executorService = Executors.newSingleThreadExecutor(); + final TimeLimiter timeLimiter = timeLimiterProvider.getTimeLimiter(executorService); + + try { + timeLimiter.callWithTimeout(() -> { + applyPessimisticWriteLockOnAnchor(sessionId, dataspaceName, anchorName); + return null; + }, timeoutInMilliseconds, TimeUnit.MILLISECONDS); + } catch (final TimeoutException e) { + throw new SessionTimeoutException( + "Timeout: Anchor locking failed", + "The error could be caused by another session holding a lock on the specified table. " + + "Retrying the sending the request could be required.", e); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new SessionManagerException("Operation interrupted", "This thread was interrupted.", e); + } catch (final ExecutionException | UncheckedExecutionException e) { + if (e.getCause() != null) { + throw e.getCause(); + } + throw new SessionManagerException( + "Operation Aborted", + "The transaction request was aborted. " + + "Retrying and checking all details are correct could be required", e); + } finally { + executorService.shutdownNow(); + } + } + + private void applyPessimisticWriteLockOnAnchor(final String sessionId, final String dataspaceName, + final String anchorName) { + final Session session = getSession(sessionId); + final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); + final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); + final int anchorId = anchorEntity.getId(); + log.debug("Attempting to lock anchor {} for session {}", anchorName, sessionId); + session.get(AnchorEntity.class, anchorId, LockMode.PESSIMISTIC_WRITE); + log.info("Anchor {} successfully locked", anchorName); + } + + private Session getSession(final String sessionId) { + final Session session = sessionMap.get(sessionId); + if (session == null) { + throw new SessionManagerException("Session not found", + String.format("Session with ID %s does not exist", sessionId)); + } + return session; + } +} diff --git a/cps-ri/src/main/java/org/onap/cps/spi/utils/TimeLimiterProvider.java b/cps-ri/src/main/java/org/onap/cps/spi/utils/TimeLimiterProvider.java new file mode 100644 index 0000000000..2bd7ac3763 --- /dev/null +++ b/cps-ri/src/main/java/org/onap/cps/spi/utils/TimeLimiterProvider.java @@ -0,0 +1,33 @@ +/* + * ============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.spi.utils; + +import com.google.common.util.concurrent.SimpleTimeLimiter; +import com.google.common.util.concurrent.TimeLimiter; +import java.util.concurrent.ExecutorService; +import org.springframework.stereotype.Component; + +@Component +public class TimeLimiterProvider { + public TimeLimiter getTimeLimiter(final ExecutorService executorService) { + return SimpleTimeLimiter.create(executorService); + } +} diff --git a/cps-ri/src/main/resources/hibernate.cfg.xml b/cps-ri/src/main/resources/hibernate.cfg.xml new file mode 100644 index 0000000000..98e6cfc5b7 --- /dev/null +++ b/cps-ri/src/main/resources/hibernate.cfg.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE hibernate-configuration PUBLIC + "-//Hibernate/Hibernate Configuration DTD 3.0//EN" + "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> + +<hibernate-configuration> + <session-factory> + <property name="hibernate.connection.driver_class">org.postgresql.Driver</property> + <property name="hibernate.connection.url">jdbc:postgresql://${DB_HOST}:${DB_PORT:5432}/cpsdb</property> + <property name="hibernate.connection.username">${DB_USERNAME}</property> + <property name="hibernate.connection.password">${DB_PASSWORD}</property> + <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQL82Dialect</property> + <property name="show_sql">true</property> + <property name="hibernate.hbm2ddl.auto">update</property> + </session-factory> +</hibernate-configuration>
\ No newline at end of file diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy index 063bd5b5ae..2de087fc28 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Nordix Foundation + * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ @@ -22,6 +22,7 @@ package org.onap.cps.spi.impl +import org.mockito.Mock import org.onap.cps.spi.CpsAdminPersistenceService import org.onap.cps.spi.exceptions.AlreadyDefinedException import org.onap.cps.spi.exceptions.AnchorNotFoundException @@ -30,15 +31,21 @@ import org.onap.cps.spi.exceptions.DataspaceNotFoundException import org.onap.cps.spi.exceptions.SchemaSetNotFoundException import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException import org.onap.cps.spi.model.Anchor +import org.onap.cps.spi.model.CmHandleQueryParameters import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.context.jdbc.Sql +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase { @Autowired CpsAdminPersistenceService objectUnderTest + @Mock + ObjectMapper objectMapper + static final String SET_DATA = '/data/anchor.sql' + static final String SET_FRAGMENT_DATA = '/data/fragment.sql' static final String SAMPLE_DATA_FOR_ANCHORS_WITH_MODULES = '/data/anchors-schemaset-modules.sql' static final String DATASPACE_WITH_NO_DATA = 'DATASPACE-002-NO-DATA' static final Integer DELETED_ANCHOR_ID = 3002 @@ -46,7 +53,7 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase { @Sql(CLEAR_DATA) def 'Create and retrieve a new dataspace.'() { when: 'a new dataspace is created' - def dataspaceName = 'some new dataspace' + def dataspaceName = 'some-new-dataspace' objectUnderTest.createDataspace(dataspaceName) then: 'that dataspace can be retrieved from the dataspace repository' def dataspaceEntity = dataspaceRepository.findByName(dataspaceName).orElseThrow() @@ -66,7 +73,7 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase { @Sql([CLEAR_DATA, SET_DATA]) def 'Create and retrieve a new anchor.'() { when: 'a new anchor is created' - def newAnchorName = 'my new anchor' + def newAnchorName = 'my-new-anchor' objectUnderTest.createAnchor(DATASPACE_NAME, SCHEMA_SET_NAME1, newAnchorName) then: 'that anchor can be retrieved' def anchor = objectUnderTest.getAnchor(DATASPACE_NAME, newAnchorName) @@ -141,7 +148,7 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase { @Sql(CLEAR_DATA) def 'Get all anchors in unknown dataspace.'() { when: 'attempt to get all anchors in an unknown dataspace' - objectUnderTest.getAnchors('unknown dataspace') + objectUnderTest.getAnchors('unknown-dataspace') then: 'an DataspaceNotFoundException is thrown' thrown(DataspaceNotFoundException) } @@ -219,4 +226,20 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase { 'dataspace contains schemasets' | 'DATASPACE-003' || DataspaceInUseException | 'contains 1 schemaset(s)' } + @Sql([CLEAR_DATA, SET_FRAGMENT_DATA]) + def 'Retrieve cm handle ids when #scenario.'() { + when: 'the service is invoked' + def cmHandleQueryParameters = new CmHandleQueryParameters() + cmHandleQueryParameters.setPublicProperties(publicProperties) + def returnedCmHandles = objectUnderTest.queryCmHandles(cmHandleQueryParameters) + then: 'the correct expected cm handles are returned' + returnedCmHandles == expectedCmHandleIds + where: 'the following data is used' + scenario | publicProperties || expectedCmHandleIds + 'single matching property' | ['Contact' : 'newemailforstore@bookstore.com'] || ['PNFDemo2', 'PNFDemo', 'PNFDemo4'] as Set + 'public property dont match' | ['wont_match' : 'wont_match'] || [] as Set + '2 properties, only one match (and)' | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': 'newemailforstore2@bookstore.com'] || ['PNFDemo4'] as Set + '2 properties, no match (and)' | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': ''] || [] as Set + 'No public properties - return all cm handles' | [ : ] || ['PNFDemo3', 'PNFDemo', 'PNFDemo2', 'PNFDemo4'] as Set + } } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy index ae88d302bb..36b378a775 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Nordix Foundation + * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021 Bell Canada. * ================================================================================ @@ -92,15 +92,15 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase { } where: 'the following data is used' scenario | cpsPath || expectedXPaths - 'fully unique descendant name' | '//categories[@code=2]' || ['/shops/shop[@id=1]/categories[@code=2]', '/shops/shop[@id=2]/categories[@code=1]', '/shops/shop[@id=2]/categories[@code=2]'] - 'descendant name match end of other node' | '//book' || ['/shops/shop[@id=1]/categories[@code=1]/book', '/shops/shop[@id=1]/categories[@code=2]/book'] - 'descendant with text condition on leaf' | '//book/title[text()="Chapters"]' || ['/shops/shop[@id=1]/categories[@code=2]/book'] + 'fully unique descendant name' | '//categories[@code=2]' || ["/shops/shop[@id='1']/categories[@code='2']", "/shops/shop[@id='2']/categories[@code='1']", "/shops/shop[@id='2']/categories[@code='2']"] + 'descendant name match end of other node' | '//book' || ["/shops/shop[@id='1']/categories[@code='1']/book", "/shops/shop[@id='1']/categories[@code='2']/book"] + 'descendant with text condition on leaf' | '//book/title[text()="Chapters"]' || ["/shops/shop[@id='1']/categories[@code='2']/book"] 'descendant with text condition case mismatch' | '//book/title[text()="chapters"]' || [] - 'descendant with text condition on int leaf' | '//book/price[text()="5"]' || ['/shops/shop[@id=1]/categories[@code=1]/book'] - 'descendant with text condition on leaf-list' | '//book/labels[text()="special offer"]' || ['/shops/shop[@id=1]/categories[@code=1]/book'] + 'descendant with text condition on int leaf' | '//book/price[text()="5"]' || ["/shops/shop[@id='1']/categories[@code='1']/book"] + 'descendant with text condition on leaf-list' | '//book/labels[text()="special offer"]' || ["/shops/shop[@id='1']/categories[@code='1']/book"] 'descendant with text condition partial match' | '//book/labels[text()="special"]' || [] - 'descendant with text condition (existing) empty string' | '//book/labels[text()=""]' || ['/shops/shop[@id=1]/categories[@code=1]/book'] - 'descendant with text condition on int leaf-list' | '//book/editions[text()="2000"]' || ['/shops/shop[@id=1]/categories[@code=2]/book'] + 'descendant with text condition (existing) empty string' | '//book/labels[text()=""]' || ["/shops/shop[@id='1']/categories[@code='1']/book"] + 'descendant with text condition on int leaf-list' | '//book/editions[text()="2000"]' || ["/shops/shop[@id='1']/categories[@code='2']/book"] } @Sql([CLEAR_DATA, SET_DATA]) @@ -115,10 +115,10 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase { } where: 'the following data is used' scenario | cpsPath || expectedXPaths - 'one leaf' | '//author[@FirstName="Joe"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]'] - 'more than one leaf' | '//author[@FirstName="Joe" and @Surname="Bloggs"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]'] - 'leaves reversed in order' | '//author[@Surname="Bloggs" and @FirstName="Joe"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]'] - 'leaf and text condition' | '//author[@FirstName="Joe"]/Surname[text()="Bloggs"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]'] + 'one leaf' | '//author[@FirstName="Joe"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']", "/shops/shop[@id='1']/categories[@code='2']/book/author[@FirstName='Joe' and @Surname='Smith']"] + 'more than one leaf' | '//author[@FirstName="Joe" and @Surname="Bloggs"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"] + 'leaves reversed in order' | '//author[@Surname="Bloggs" and @FirstName="Joe"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"] + 'leaf and text condition' | '//author[@FirstName="Joe"]/Surname[text()="Bloggs"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"] } @Sql([CLEAR_DATA, SET_DATA]) @@ -133,9 +133,9 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase { } where: 'the following data is used' scenario | cpsPath || expectedXPaths - 'one partial key leaf' | '//author[@FirstName="Joe"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]'] - 'one non key leaf' | '//author[@title="Dune"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]'] - 'mix of partial key and non key leaf' | '//author[@FirstName="Joe" and @title="Dune"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]'] + 'one partial key leaf' | '//author[@FirstName="Joe"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']", "/shops/shop[@id='1']/categories[@code='2']/book/author[@FirstName='Joe' and @Surname='Smith']"] + 'one non key leaf' | '//author[@title="Dune"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"] + 'mix of partial key and non key leaf' | '//author[@FirstName="Joe" and @title="Dune"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"] } @Sql([CLEAR_DATA, SET_DATA]) @@ -149,13 +149,13 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase { } where: 'the following data is used' scenario | cpsPath || expectedXPaths - 'multiple list-ancestors' | '//book/ancestor::categories' || ['/shops/shop[@id=1]/categories[@code=1]', '/shops/shop[@id=1]/categories[@code=2]'] - 'one ancestor with list value' | '//book/ancestor::categories[@code=1]' || ['/shops/shop[@id=1]/categories[@code=1]'] + 'multiple list-ancestors' | '//book/ancestor::categories' || ["/shops/shop[@id='1']/categories[@code='1']", "/shops/shop[@id='1']/categories[@code='2']"] + 'one ancestor with list value' | '//book/ancestor::categories[@code=1]' || ["/shops/shop[@id='1']/categories[@code='1']"] 'top ancestor' | '//shop[@id=1]/ancestor::shops' || ['/shops'] - 'list with index value in the xpath prefix' | '//categories[@code=1]/book/ancestor::shop[@id=1]' || ['/shops/shop[@id=1]'] - 'ancestor with parent list' | '//book/ancestor::shop[@id=1]/categories[@code=2]' || ['/shops/shop[@id=1]/categories[@code=2]'] - 'ancestor with parent' | '//phonenumbers[@type="mob"]/ancestor::info/contact' || ['/shops/shop[@id=3]/info/contact'] - 'ancestor combined with text condition' | '//book/title[text()="Dune"]/ancestor::shop' || ['/shops/shop[@id=1]'] + 'list with index value in the xpath prefix' | '//categories[@code=1]/book/ancestor::shop[@id=1]' || ["/shops/shop[@id='1']"] + 'ancestor with parent list' | '//book/ancestor::shop[@id=1]/categories[@code=2]' || ["/shops/shop[@id='1']/categories[@code='2']"] + 'ancestor with parent' | '//phonenumbers[@type="mob"]/ancestor::info/contact' || ["/shops/shop[@id='3']/info/contact"] + 'ancestor combined with text condition' | '//book/title[text()="Dune"]/ancestor::shop' || ["/shops/shop[@id='1']"] 'ancestor with parent that does not exist' | '//book/ancestor::parentDoesNoExist/categories' || [] 'ancestor does not exist' | '//book/ancestor::ancestorDoesNotExist' || [] } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy index ab290051a2..6f780fc508 100755 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy @@ -23,11 +23,13 @@ package org.onap.cps.spi.impl import com.fasterxml.jackson.databind.ObjectMapper import com.google.common.collect.ImmutableSet +import org.onap.cps.cpspath.parser.PathParsingException import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.entities.FragmentEntity import org.onap.cps.spi.exceptions.AlreadyDefinedException import org.onap.cps.spi.exceptions.AnchorNotFoundException import org.onap.cps.spi.exceptions.CpsAdminException +import org.onap.cps.spi.exceptions.CpsPathException import org.onap.cps.spi.exceptions.DataNodeNotFoundException import org.onap.cps.spi.exceptions.DataspaceNotFoundException import org.onap.cps.spi.model.DataNode @@ -150,7 +152,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { thrown(expectedException) where: 'the following data is used' scenario | parentXpath | dataNode || expectedException - 'parent does not exist' | 'unknown' | newDataNode || DataNodeNotFoundException + 'parent does not exist' | '/unknown' | newDataNode || DataNodeNotFoundException 'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || AlreadyDefinedException } @@ -185,9 +187,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { then: 'a #expectedException is thrown' thrown(expectedException) where: 'following parameters were used' - scenario | parentNodeXpath | listElementXpaths || expectedException - 'parent node does not exist' | '/unknown' | ['irrelevant'] || DataNodeNotFoundException - 'already existing fragment' | '/parent-201' | ['/parent-201/child-204[@key="A"]'] || AlreadyDefinedException + scenario | parentNodeXpath | listElementXpaths || expectedException + 'parent node does not exist' | '/unknown' | ['irrelevant'] || DataNodeNotFoundException + 'data fragment already exists' | '/parent-201' | ["/parent-201/child-204[@key='A']"] || AlreadyDefinedException } @Sql([CLEAR_DATA, SET_DATA]) @@ -208,6 +210,15 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) + def 'Cps Path query with syntax error throws a CPS Path Exception.'() { + when: 'trying to execute a query with a syntax (parsing) error' + objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS) + then: 'exception is thrown' + def exceptionThrown = thrown(CpsPathException) + assert exceptionThrown.getDetails().contains('failed to parse at line 1 due to extraneous input \'invalid-cps-path\' expecting \'/\'') + } + + @Sql([CLEAR_DATA, SET_DATA]) def 'Get data node by xpath with all descendants.'() { when: 'data node is requested with all descendants' def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, @@ -235,10 +246,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { then: 'a #expectedException is thrown' thrown(expectedException) where: 'the following data is used' - scenario | dataspaceName | anchorName | xpath || expectedException - 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException - 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException - 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH' || DataNodeNotFoundException + scenario | dataspaceName | anchorName | xpath || expectedException + 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | '/not relevant' || DataspaceNotFoundException + 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | '/not relevant' || AnchorNotFoundException + 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NO XPATH' || DataNodeNotFoundException } @Sql([CLEAR_DATA, SET_DATA]) @@ -265,10 +276,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { then: 'a #expectedException is thrown' thrown(expectedException) where: 'the following data is used' - scenario | dataspaceName | anchorName | xpath || expectedException - 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException - 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException - 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException + scenario | dataspaceName | anchorName | xpath || expectedException + 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | '/not relevant' || DataspaceNotFoundException + 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | '/not relevant' || AnchorNotFoundException + 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING XPATH' || DataNodeNotFoundException } @Sql([CLEAR_DATA, SET_DATA]) @@ -359,10 +370,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { then: 'a #expectedException is thrown' thrown(expectedException) where: 'the following data is used' - scenario | dataspaceName | anchorName | xpath || expectedException - 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException - 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException - 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException + scenario | dataspaceName | anchorName | xpath || expectedException + 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | '/not relevant' || DataspaceNotFoundException + 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | '/not relevant' || AnchorNotFoundException + 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING XPATH' || DataNodeNotFoundException } @Sql([CLEAR_DATA, SET_DATA]) @@ -468,10 +479,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths) where: 'following parameters were used' scenario | targetXpaths | parentFragmentId || expectedRemainingChildXpaths - 'list element with key' | '/parent-203/child-204[@key="A"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="B"]'] - 'list element with combined keys' | '/parent-202/child-205[@key="A" and @key2="B"]' | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ['/parent-202/child-206[@key="A"]'] + 'list element with key' | '/parent-203/child-204[@key="A"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ["/parent-203/child-203", "/parent-203/child-204[@key='B']"] + 'list element with combined keys' | '/parent-202/child-205[@key="A" and @key2="B"]' | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ["/parent-202/child-206[@key='A']"] 'whole list' | '/parent-203/child-204' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203'] - 'list element under list element' | '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="A"]', '/parent-203/child-204[@key="B"]'] + 'list element under list element' | '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ["/parent-203/child-203", "/parent-203/child-204[@key='A']", "/parent-203/child-204[@key='B']"] } @Sql([CLEAR_DATA, SET_DATA]) @@ -510,9 +521,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { 'child of target' | '/parent-206/child-206' | '/parent-206/child-206' || null 'child data node, parent still exists' | '/parent-206/child-206' | '/parent-206' || '/parent-206' 'list element' | '/parent-206/child-206/grand-child-206[@key="A"]' | '/parent-206/child-206/grand-child-206[@key="A"]' || null - 'list element, sibling still exists' | '/parent-206/child-206/grand-child-206[@key="A"]' | '/parent-206/child-206/grand-child-206[@key="X"]' || '/parent-206/child-206/grand-child-206[@key="X"]' + 'list element, sibling still exists' | '/parent-206/child-206/grand-child-206[@key="A"]' | '/parent-206/child-206/grand-child-206[@key="X"]' || "/parent-206/child-206/grand-child-206[@key='X']" 'container node' | '/parent-206' | '/parent-206' || null - 'container list node' | '/parent-206[@key="A"]' | '/parent-206[@key="B"]' || '/parent-206[@key="B"]' + 'container list node' | '/parent-206[@key="A"]' | '/parent-206[@key="B"]' || "/parent-206[@key='B']" 'root node with xpath /' | '/' | '/' || null 'root node with xpath passed as blank' | '' | '' || null @@ -523,11 +534,11 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { when: 'data node is deleted' objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, datanodeXpath) then: 'a #expectedException is thrown' - thrown(DataNodeNotFoundException) + thrown(expectedException) where: 'the following parameters were used' - scenario | datanodeXpath - 'valid data node, non existent child node' | '/parent-203/child-non-existent' - 'invalid list element' | '/parent-206/child-206/grand-child-206@key="A"]' + scenario | datanodeXpath | expectedException + 'valid data node, non existent child node' | '/parent-203/child-non-existent' | DataNodeNotFoundException + 'invalid list element' | '/parent-206/child-206/grand-child-206@key="A"]' | PathParsingException } @Sql([CLEAR_DATA, SET_DATA]) diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy index 7166008ad3..b37f471e76 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (c) 2021 Bell Canada. + * Modifications 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. @@ -28,88 +29,111 @@ import org.onap.cps.spi.model.DataNodeBuilder import org.onap.cps.spi.repository.AnchorRepository import org.onap.cps.spi.repository.DataspaceRepository import org.onap.cps.spi.repository.FragmentRepository +import org.onap.cps.spi.utils.SessionManager import org.onap.cps.utils.JsonObjectMapper import spock.lang.Specification - class CpsDataPersistenceServiceSpec extends Specification { def mockDataspaceRepository = Mock(DataspaceRepository) def mockAnchorRepository = Mock(AnchorRepository) def mockFragmentRepository = Mock(FragmentRepository) def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + def mockSessionManager = Mock(SessionManager) def objectUnderTest = new CpsDataPersistenceServiceImpl( - mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper) + mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper,mockSessionManager) def 'Handling of StaleStateException (caused by concurrent updates) during data node tree update.'() { - def parentXpath = 'parent-01' + def parentXpath = '/parent-01' def myDataspaceName = 'my-dataspace' def myAnchorName = 'my-anchor' given: 'data node object' - def submittedDataNode = new DataNodeBuilder() - .withXpath(parentXpath) - .withLeaves(['leaf-name': 'leaf-value']) - .build() + def submittedDataNode = new DataNodeBuilder() + .withXpath(parentXpath) + .withLeaves(['leaf-name': 'leaf-value']) + .build() and: 'fragment to be updated' - mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> { - def fragmentEntity = new FragmentEntity() - fragmentEntity.setXpath(parentXpath) - fragmentEntity.setChildFragments(Collections.emptySet()) - return fragmentEntity - } + mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> { + def fragmentEntity = new FragmentEntity() + fragmentEntity.setXpath(parentXpath) + fragmentEntity.setChildFragments(Collections.emptySet()) + return fragmentEntity + } and: 'data node is concurrently updated by another transaction' - mockFragmentRepository.save(_) >> { throw new StaleStateException("concurrent updates") } + mockFragmentRepository.save(_) >> { throw new StaleStateException("concurrent updates") } when: 'attempt to update data node' - objectUnderTest.replaceDataNodeTree(myDataspaceName, myAnchorName, submittedDataNode) + objectUnderTest.replaceDataNodeTree(myDataspaceName, myAnchorName, submittedDataNode) then: 'concurrency exception is thrown' - def concurrencyException = thrown(ConcurrencyException) - assert concurrencyException.getDetails().contains(myDataspaceName) - assert concurrencyException.getDetails().contains(myAnchorName) - assert concurrencyException.getDetails().contains(parentXpath) + def concurrencyException = thrown(ConcurrencyException) + assert concurrencyException.getDetails().contains(myDataspaceName) + assert concurrencyException.getDetails().contains(myAnchorName) + assert concurrencyException.getDetails().contains(parentXpath) } def 'Retrieving a data node with a property JSON value of #scenario'() { given: 'a fragment with a property JSON value of #scenario' - mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> { - new FragmentEntity(childFragments: Collections.emptySet(), - attributes: "{\"some attribute\": ${dataString}}") - } + mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> { + new FragmentEntity(childFragments: Collections.emptySet(), + attributes: "{\"some attribute\": ${dataString}}") + } when: 'getting the data node represented by this fragment' - def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor', - 'parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor', + '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) then: 'the leaf is of the correct value and data type' - def attributeValue = dataNode.leaves.get('some attribute') - assert attributeValue == expectedValue - assert attributeValue.class == expectedDataClass + def attributeValue = dataNode.leaves.get('some attribute') + assert attributeValue == expectedValue + assert attributeValue.class == expectedDataClass where: 'the following Data Type is passed' - scenario | dataString || expectedValue | expectedDataClass - 'just numbers' | '15174' || 15174 | Integer - 'number with dot' | '15174.32' || 15174.32 | Double - 'number with 0 value after dot' | '15174.0' || 15174.0 | Double - 'number with 0 value before dot' | '0.32' || 0.32 | Double - 'number higher than max int' | '2147483648' || 2147483648 | Long - 'just text' | '"Test"' || 'Test' | String - 'number with exponent' | '1.2345e5' || 1.2345e5 | Double - 'number higher than max int with dot' | '123456789101112.0' || 123456789101112.0 | Double - 'text and numbers' | '"String = \'1234\'"' || "String = '1234'" | String - 'number as String' | '"12345"' || '12345' | String + scenario | dataString || expectedValue | expectedDataClass + 'just numbers' | '15174' || 15174 | Integer + 'number with dot' | '15174.32' || 15174.32 | Double + 'number with 0 value after dot' | '15174.0' || 15174.0 | Double + 'number with 0 value before dot' | '0.32' || 0.32 | Double + 'number higher than max int' | '2147483648' || 2147483648 | Long + 'just text' | '"Test"' || 'Test' | String + 'number with exponent' | '1.2345e5' || 1.2345e5 | Double + 'number higher than max int with dot' | '123456789101112.0' || 123456789101112.0 | Double + 'text and numbers' | '"String = \'1234\'"' || "String = '1234'" | String + 'number as String' | '"12345"' || '12345' | String } def 'Retrieving a data node with invalid JSON'() { given: 'a fragment with invalid JSON' - mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> { - new FragmentEntity(childFragments: Collections.emptySet(), attributes: '{invalid json') - } + mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> { + new FragmentEntity(childFragments: Collections.emptySet(), attributes: '{invalid json') + } when: 'getting the data node represented by this fragment' - def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor', - 'parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor', + '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) then: 'a data validation exception is thrown' - thrown(DataValidationException) + thrown(DataValidationException) + } + + def 'start session'() { + when: 'start session' + objectUnderTest.startSession() + then: 'the session manager method to start session is invoked' + 1 * mockSessionManager.startSession() } -} + def 'close session'() { + given: 'session ID' + def someSessionId = 'someSessionId' + when: 'close session method is called with session ID as parameter' + objectUnderTest.closeSession(someSessionId) + then: 'the session manager method to close session is invoked with parameter' + 1 * mockSessionManager.closeSession(someSessionId) + } + + def 'Lock anchor.'(){ + when: 'lock anchor method is called with anchor entity details' + objectUnderTest.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L) + then: 'the session manager method to lock anchor is invoked with same parameters' + 1 * mockSessionManager.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L) + } +}
\ No newline at end of file diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy new file mode 100644 index 0000000000..9b58c8bc32 --- /dev/null +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy @@ -0,0 +1,69 @@ +/* + * ============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.spi.utils + +import org.onap.cps.spi.exceptions.SessionManagerException +import org.onap.cps.spi.impl.CpsPersistenceSpecBase +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.jdbc.Sql + +class SessionManagerIntegrationSpec extends CpsPersistenceSpecBase{ + + final static String SET_DATA = '/data/anchor.sql' + + @Autowired + SessionManager objectUnderTest + + def sessionId + def shortTimeoutForTesting = 200L + + def setup(){ + sessionId = objectUnderTest.startSession() + } + + def cleanup(){ + objectUnderTest.closeSession(sessionId) + } + + @Sql([CLEAR_DATA, SET_DATA]) + def 'Lock anchor.'(){ + when: 'session tries to acquire anchor lock by passing anchor entity details' + objectUnderTest.lockAnchor(sessionId, DATASPACE_NAME, ANCHOR_NAME1, shortTimeoutForTesting) + then: 'no exception is thrown' + noExceptionThrown() + } + + @Sql([CLEAR_DATA, SET_DATA]) + def 'Attempt to lock anchor when another session is holding the lock.'(){ + given: 'another session that holds an anchor lock' + def otherSessionId = objectUnderTest.startSession() + objectUnderTest.lockAnchor(otherSessionId,DATASPACE_NAME,ANCHOR_NAME1,shortTimeoutForTesting) + when: 'a session tries to acquire the same anchor lock' + objectUnderTest.lockAnchor(sessionId,DATASPACE_NAME,ANCHOR_NAME1,shortTimeoutForTesting) + then: 'a session manager exception is thrown specifying operation reached timeout' + def thrown = thrown(SessionManagerException) + thrown.message.contains('Timeout') + then: 'when the other session holding the lock is closed, lock can finally be acquired' + objectUnderTest.closeSession(otherSessionId) + objectUnderTest.lockAnchor(sessionId,DATASPACE_NAME,ANCHOR_NAME1,shortTimeoutForTesting) + } + +} diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy new file mode 100644 index 0000000000..a2df06ef0e --- /dev/null +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy @@ -0,0 +1,99 @@ +/* + * ============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.spi.utils + +import com.google.common.util.concurrent.TimeLimiter +import org.hibernate.HibernateException +import org.hibernate.Transaction +import org.onap.cps.spi.entities.AnchorEntity +import org.onap.cps.spi.exceptions.SessionManagerException +import org.onap.cps.spi.repository.AnchorRepository +import org.onap.cps.spi.repository.DataspaceRepository +import org.testcontainers.shaded.com.google.common.util.concurrent.UncheckedExecutionException +import spock.lang.Specification +import org.hibernate.Session + +import java.util.concurrent.ExecutionException + +class SessionManagerSpec extends Specification { + + def spiedTimeLimiterProvider = Spy(TimeLimiterProvider) + def mockDataspaceRepository = Mock(DataspaceRepository) + def mockAnchorRepository = Mock(AnchorRepository) + def mockSession = Mock(Session) + + def objectUnderTest = new SessionManager(spiedTimeLimiterProvider, mockDataspaceRepository, mockAnchorRepository) + + def 'Lock anchor entity with #exceptionDuringTest exception.'(){ + given: 'a dummy session' + objectUnderTest.sessionMap.put('dummySession', mockSession) + and: 'the anchor name can be resolved' + def mockAnchorEntity = Mock(AnchorEntity) + mockAnchorEntity.getId() > 456 + mockAnchorRepository.getByDataspaceAndName(_, _) >> mockAnchorEntity + and: 'timeLimiter throws an #exceptionDuringTest exception' + def mockTimeLimiter = Mock(TimeLimiter) + spiedTimeLimiterProvider.getTimeLimiter(_) >> mockTimeLimiter + mockTimeLimiter.callWithTimeout(*_) >> { throw exceptionDuringTest } + when: 'session tries to acquire anchor lock' + objectUnderTest.lockAnchor('dummySession', 'some-dataspace','some-anchor', 123L) + then: 'a session manager exception is thrown with the expected detail' + def thrown = thrown(SessionManagerException) + thrown.details.contains(expectedExceptionDetail) + where: + exceptionDuringTest || expectedExceptionDetail + new InterruptedException() || 'interrupted' + new ExecutionException() || 'aborted' + } + + def 'Close session that does not exist.'() { + when: 'attempt to close session that does not exist' + objectUnderTest.closeSession('unknown session id') + then: 'a session manager exception is thrown with the unknown id in the details' + def thrown = thrown(SessionManagerException) + assert thrown.details.contains('unknown session id') + } + + def 'Hibernate exception while closing session.'() { + given: 'a test session with a transaction' + objectUnderTest.sessionMap.put('testSessionId', mockSession) + mockSession.getTransaction() >> Mock(Transaction) + and: 'an hibernate exception when closing that session' + def hibernateException = new HibernateException('test') + mockSession.close() >> { throw hibernateException } + when: 'attempt to close session' + objectUnderTest.closeSession('testSessionId') + then: 'a session manager exception is thrown with the session id in the details' + def thrown = thrown(SessionManagerException) + assert thrown.details.contains('testSessionId') + and: 'the original exception as cause' + assert thrown.cause == hibernateException + } + + def 'Attempt to lock anchor entity with session Id that does not exists'(){ + when: 'attempt to acquire anchor lock with session that does not exists' + objectUnderTest.lockAnchor('unknown session id','','',123L) + then: 'a session manager exception is thrown with the unknown id in the details' + def thrown = thrown(SessionManagerException) + thrown.details.contains('unknown session id') + } + +} diff --git a/cps-ri/src/test/resources/data/cps-path-query.sql b/cps-ri/src/test/resources/data/cps-path-query.sql index 8f525df6bd..d1a62209eb 100644 --- a/cps-ri/src/test/resources/data/cps-path-query.sql +++ b/cps-ri/src/test/resources/data/cps-path-query.sql @@ -1,6 +1,6 @@ /* ============LICENSE_START======================================================= - Copyright (C) 2021 Nordix Foundation. + Copyright (C) 2021-2022 Nordix Foundation. Modifications Copyright (C) 2021 Bell Canada. ================================================================================ Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,25 +30,25 @@ INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES (1, 1001, 1003, null, '/shops', null), - (2, 1001, 1003, 1, '/shops/shop[@id=1]', '{"id" : 1, "type" : "bookstore"}'), - (3, 1001, 1003, 2, '/shops/shop[@id=1]/categories[@code=1]', '{"code" : 1, "type" : "bookstore", "name": "SciFi"}'), - (4, 1001, 1003, 2, '/shops/shop[@id=1]/categories[@code=2]', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}'), - (5, 1001, 1003, 3, '/shops/shop[@id=1]/categories[@code=1]/book', '{"price" : 5, "title" : "Dune", "labels" : ["special offer","classics",""]}'), - (6, 1001, 1003, 4, '/shops/shop[@id=1]/categories[@code=2]/book', '{"price" : 15, "title" : "Chapters", "editions" : [2000,2010,2020]}'), - (7, 1001, 1003, 5, '/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '{"FirstName" : "Joe", "Surname": "Bloggs","title": "Dune"}'), - (8, 1001, 1003, 6, '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]', '{"FirstName" : "Joe", "Surname": "Smith","title": "Chapters"}'); + (2, 1001, 1003, 1, '/shops/shop[@id=''1'']', '{"id" : 1, "type" : "bookstore"}'), + (3, 1001, 1003, 2, '/shops/shop[@id=''1'']/categories[@code=''1'']', '{"code" : 1, "type" : "bookstore", "name": "SciFi"}'), + (4, 1001, 1003, 2, '/shops/shop[@id=''1'']/categories[@code=''2'']', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}'), + (5, 1001, 1003, 3, '/shops/shop[@id=''1'']/categories[@code=''1'']/book', '{"price" : 5, "title" : "Dune", "labels" : ["special offer","classics",""]}'), + (6, 1001, 1003, 4, '/shops/shop[@id=''1'']/categories[@code=''2'']/book', '{"price" : 15, "title" : "Chapters", "editions" : [2000,2010,2020]}'), + (7, 1001, 1003, 5, '/shops/shop[@id=''1'']/categories[@code=''1'']/book/author[@FirstName=''Joe'' and @Surname=''Bloggs'']', '{"FirstName" : "Joe", "Surname": "Bloggs","title": "Dune"}'), + (8, 1001, 1003, 6, '/shops/shop[@id=''1'']/categories[@code=''2'']/book/author[@FirstName=''Joe'' and @Surname=''Smith'']', '{"FirstName" : "Joe", "Surname": "Smith","title": "Chapters"}'); INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES - (9, 1001, 1003, 1, '/shops/shop[@id=2]', '{"type" : "bookstore"}'), - (10, 1001, 1003, 9, '/shops/shop[@id=2]/categories[@code=1]', '{"code" : 2, "type" : "bookstore", "name": "Kids"}'), - (11, 1001, 1003, 10, '/shops/shop[@id=2]/categories[@code=2]', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}'); + (9, 1001, 1003, 1, '/shops/shop[@id=''2'']', '{"type" : "bookstore"}'), + (10, 1001, 1003, 9, '/shops/shop[@id=''2'']/categories[@code=''1'']', '{"code" : 2, "type" : "bookstore", "name": "Kids"}'), + (11, 1001, 1003, 10, '/shops/shop[@id=''2'']/categories[@code=''2'']', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}'); INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES - (12, 1001, 1003, 1, '/shops/shop[@id=3]', '{"type" : "garden centre"}'), - (13, 1001, 1003, 12, '/shops/shop[@id=3]/categories[@code=1]', '{"id" : 1, "type" : "garden centre", "name": "indoor plants"}'), - (14, 1001, 1003, 12, '/shops/shop[@id=3]/categories[@code=2]', '{"id" : 2, "type" : "garden centre", "name": "outdoor plants"}'), - (16, 1001, 1003, 1, '/shops/shop[@id=3]/info', null), - (17, 1001, 1003, 1, '/shops/shop[@id=3]/info/contact', null), - (18, 1001, 1003, 1, '/shops/shop[@id=3]/info/contact/website', '{"address" : "myshop.ie"}'), - (19, 1001, 1003, 12, '/shops/shop[@id=3]/info/contact/phonenumbers[@type="mob"]', '{"type" : "mob", "number" : "123123456"}'), - (20, 1001, 1003, 12, '/shops/shop[@id=3]/info/contact/phonenumbers[@type="landline"]', '{"type" : "landline", "number" : "012123456"}'); + (12, 1001, 1003, 1, '/shops/shop[@id=''3'']', '{"type" : "garden centre"}'), + (13, 1001, 1003, 12, '/shops/shop[@id=''3'']/categories[@code=''1'']', '{"id" : 1, "type" : "garden centre", "name": "indoor plants"}'), + (14, 1001, 1003, 12, '/shops/shop[@id=''3'']/categories[@code=''2'']', '{"id" : 2, "type" : "garden centre", "name": "outdoor plants"}'), + (16, 1001, 1003, 1, '/shops/shop[@id=''3'']/info', null), + (17, 1001, 1003, 1, '/shops/shop[@id=''3'']/info/contact', null), + (18, 1001, 1003, 1, '/shops/shop[@id=''3'']/info/contact/website', '{"address" : "myshop.ie"}'), + (19, 1001, 1003, 12, '/shops/shop[@id=''3'']/info/contact/phonenumbers[@type=''mob'']', '{"type" : "mob", "number" : "123123456"}'), + (20, 1001, 1003, 12, '/shops/shop[@id=''3'']/info/contact/phonenumbers[@type=''landline'']', '{"type" : "landline", "number" : "012123456"}'); diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql index a27bb5fdea..4106541061 100755 --- a/cps-ri/src/test/resources/data/fragment.sql +++ b/cps-ri/src/test/resources/data/fragment.sql @@ -1,6 +1,6 @@ /* ============LICENSE_START======================================================= - Copyright (C) 2021 Nordix Foundation. + Copyright (C) 2021-2022 Nordix Foundation. Modifications Copyright (C) 2021 Pantheon.tech Modifications Copyright (C) 2021-2022 Bell Canada. ================================================================================ @@ -21,14 +21,16 @@ */ INSERT INTO DATASPACE (ID, NAME) VALUES - (1001, 'DATASPACE-001'); + (1001, 'DATASPACE-001'), + (1002, 'NCMP-Admin'); INSERT INTO SCHEMA_SET (ID, NAME, DATASPACE_ID) VALUES (2001, 'SCHEMA-SET-001', 1001); INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES (3001, 'ANCHOR-001', 1001, 2001), - (3003, 'ANCHOR-003', 1001, 2001); + (3003, 'ANCHOR-003', 1001, 2001), + (3004, 'ncmp-dmi-registry', 1002, 2001); INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH) VALUES (4001, 1001, 3001, null, '/parent-1'), @@ -50,21 +52,32 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) (4203, 1001, 3003, 4202, '/parent-200/child-201/grand-child', '{"leaf-value": "original"}'), (4206, 1001, 3003, null, '/parent-201', '{"leaf-value": "original"}'), (4207, 1001, 3003, 4206, '/parent-201/child-203', '{}'), - (4208, 1001, 3003, 4206, '/parent-201/child-204[@key="A"]', '{"key": "A"}'), - (4209, 1001, 3003, 4206, '/parent-201/child-204[@key="B"]', '{"key": "B"}'), + (4208, 1001, 3003, 4206, '/parent-201/child-204[@key=''A'']', '{"key": "A"}'), + (4209, 1001, 3003, 4206, '/parent-201/child-204[@key=''B'']', '{"key": "B"}'), (4211, 1001, 3003, null, '/parent-202', '{"leaf-value": "original"}'), - (4212, 1001, 3003, 4211, '/parent-202/child-205[@key="A" and @key2="B"]', '{"key": "A", "key2": "B"}'), - (4213, 1001, 3003, 4211, '/parent-202/child-206[@key="A"]', '{"key": "A"}'), + (4212, 1001, 3003, 4211, '/parent-202/child-205[@key=''A'' and @key2=''B'']', '{"key": "A", "key2": "B"}'), + (4213, 1001, 3003, 4211, '/parent-202/child-206[@key=''A'']', '{"key": "A"}'), (4214, 1001, 3003, null, '/parent-203', '{"leaf-value": "original"}'), (4215, 1001, 3003, 4214, '/parent-203/child-203', '{}'), - (4216, 1001, 3003, 4214, '/parent-203/child-204[@key="A"]', '{"key": "A"}'), - (4217, 1001, 3003, 4214, '/parent-203/child-204[@key="B"]', '{"key": "B"}'), - (4218, 1001, 3003, 4217, '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]', '{"key": "B", "key2": "Y"}'), + (4216, 1001, 3003, 4214, '/parent-203/child-204[@key=''A'']', '{"key": "A"}'), + (4217, 1001, 3003, 4214, '/parent-203/child-204[@key=''B'']', '{"key": "B"}'), + (4218, 1001, 3003, 4217, '/parent-203/child-204[@key=''B'']/grand-child-204[@key2=''Y'']', '{"key": "B", "key2": "Y"}'), (4226, 1001, 3003, null, '/parent-206', '{"leaf-value": "original"}'), (4227, 1001, 3003, 4226, '/parent-206/child-206', '{}'), (4228, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206', '{}'), - (4229, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key="A"]', '{"key": "A"}'), - (4230, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key="X"]', '{"key": "X"}'), - (4231, 1001, 3003, null, '/parent-206[@key="A"]', '{"key": "A"}'), - (4232, 1001, 3003, 4231, '/parent-206[@key="A"]/child-206', '{}'), - (4233, 1001, 3003, null, '/parent-206[@key="B"]', '{"key": "B"}');
\ No newline at end of file + (4229, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key=''A'']', '{"key": "A"}'), + (4230, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key=''X'']', '{"key": "X"}'), + (4231, 1001, 3003, null, '/parent-206[@key=''A'']', '{"key": "A"}'), + (4232, 1001, 3003, 4231, '/parent-206[@key=''A'']/child-206', '{}'), + (4233, 1001, 3003, null, '/parent-206[@key=''B'']', '{"key": "B"}'); + +INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES + (5000, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo'']', '{"id": "PNFDemo", "dmi-service-name": "http://172.21.235.14:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'), + (5001, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo2'']', '{"id": "PNFDemo2", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'), + (5002, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo3'']', '{"id": "PNFDemo3", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'), + (5003, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo4'']', '{"id": "PNFDemo4", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'), + (5004, 1002, 3004, 5000, '/dmi-registry/cm-handles[@id=''PNFDemo'']/public-properties[@name=''Contact'']', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'), + (5005, 1002, 3004, 5001, '/dmi-registry/cm-handles[@id=''PNFDemo2'']/public-properties[@name=''Contact'']', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'), + (5006, 1002, 3004, 5002, '/dmi-registry/cm-handles[@id=''PNFDemo3'']/public-properties[@name=''Contact'']', '{"name": "Contact3", "value": "PNF3@bookstore.com"}'), + (5007, 1002, 3004, 5003, '/dmi-registry/cm-handles[@id=''PNFDemo4'']/public-properties[@name=''Contact'']', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'), + (5008, 1002, 3004, 5004, '/dmi-registry/cm-handles[@id=''PNFDemo4'']/public-properties[@name=''Contact2'']', '{"name": "Contact2", "value": "newemailforstore2@bookstore.com"}'); diff --git a/cps-ri/src/test/resources/hibernate.cfg.xml b/cps-ri/src/test/resources/hibernate.cfg.xml new file mode 100644 index 0000000000..fae9275ddc --- /dev/null +++ b/cps-ri/src/test/resources/hibernate.cfg.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE hibernate-configuration PUBLIC
+ "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
+ "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
+
+<hibernate-configuration>
+ <session-factory>
+ <property name="hibernate.connection.driver_class">org.postgresql.Driver</property>
+ <property name="hibernate.connection.url">${DB_URL}</property>
+ <property name="hibernate.connection.username">${DB_USERNAME}</property>
+ <property name="hibernate.connection.password">${DB_PASSWORD}</property>
+ <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQL82Dialect</property>
+ <property name="show_sql">true</property>
+ <property name="hibernate.hbm2ddl.auto">none</property>
+ </session-factory>
+</hibernate-configuration>
\ No newline at end of file diff --git a/cps-service/pom.xml b/cps-service/pom.xml index 9c7031e2f8..aea122d176 100644 --- a/cps-service/pom.xml +++ b/cps-service/pom.xml @@ -28,7 +28,7 @@ <parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
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..2106f1584e 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-2022 Nordix Foundation * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * ================================================================================ @@ -23,9 +23,11 @@ package org.onap.cps.api; import java.util.Collection; +import java.util.Set; import org.onap.cps.spi.exceptions.AlreadyDefinedException; import org.onap.cps.spi.exceptions.CpsException; import org.onap.cps.spi.model.Anchor; +import org.onap.cps.spi.model.CmHandleQueryParameters; /** * CPS Admin Service. @@ -100,4 +102,12 @@ public interface CpsAdminService { * given module names */ Collection<String> queryAnchorNames(String dataspaceName, Collection<String> moduleNames); + + /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryParameters the cm handle query parameters + * @return collection of cm handle ids + */ + Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters); } diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java index cdd417bd8d..93c96ec650 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java @@ -174,4 +174,41 @@ public interface CpsDataService { */ void updateNodeLeavesAndExistingDescendantLeaves(String dataspaceName, String anchorName, String parentNodeXpath, String dataNodeUpdatesAsJson, OffsetDateTime observedTimestamp); + + /** + * Starts a session which allows use of locks and batch interaction with the persistence service. + * + * @return Session ID string + */ + String startSession(); + + /** + * Close session. + * + * @param sessionId session ID + * + */ + void closeSession(String sessionId); + + /** + * Lock anchor with default timeout. + * To release locks(s), the session holding the lock(s) must be closed. + * + * @param sessionID session ID + * @param dataspaceName dataspace name + * @param anchorName anchor name + */ + void lockAnchor(String sessionID, String dataspaceName, String anchorName); + + /** + * Lock anchor with custom timeout. + * To release locks(s), the session holding the lock(s) must be closed. + * + * @param sessionID session ID + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param timeoutInMilliseconds lock attempt timeout in milliseconds + */ + void lockAnchor(String sessionID, String dataspaceName, String anchorName, Long timeoutInMilliseconds); + } 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..762754f9a8 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 * ================================================================================ @@ -24,12 +24,15 @@ package org.onap.cps.api.impl; import java.time.OffsetDateTime; import java.util.Collection; +import java.util.Set; import java.util.stream.Collectors; import lombok.AllArgsConstructor; 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.spi.model.CmHandleQueryParameters; +import org.onap.cps.utils.CpsValidator; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -43,43 +46,56 @@ 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()); } + + @Override + public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) { + return cpsAdminPersistenceService.queryCmHandles(cmHandleQueryParameters); + } } 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 aae355d507..2f1067aafe 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; @@ -47,6 +48,7 @@ import org.springframework.stereotype.Service; public class CpsDataServiceImpl implements CpsDataService { private static final String ROOT_NODE_XPATH = "/"; + private static final long DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS = 300L; private final CpsDataPersistenceService cpsDataPersistenceService; private final CpsAdminService cpsAdminService; @@ -56,7 +58,8 @@ public class CpsDataServiceImpl implements CpsDataService { @Override public void saveData(final String dataspaceName, final String anchorName, final String jsonData, final OffsetDateTime observedTimestamp) { - final var dataNode = buildDataNode(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData); + CpsValidator.validateNameCharacters(dataspaceName, anchorName); + final DataNode dataNode = buildDataNode(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData); cpsDataPersistenceService.storeDataNode(dataspaceName, anchorName, dataNode); processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, ROOT_NODE_XPATH, Operation.CREATE); } @@ -64,7 +67,8 @@ public class CpsDataServiceImpl implements CpsDataService { @Override public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { - final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData); + CpsValidator.validateNameCharacters(dataspaceName, anchorName); + final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData); cpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, parentNodeXpath, dataNode); processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.CREATE); } @@ -72,6 +76,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,13 +87,15 @@ 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) { - final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData); + CpsValidator.validateNameCharacters(dataspaceName, anchorName); + final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData); cpsDataPersistenceService .updateDataLeaves(dataspaceName, anchorName, dataNode.getXpath(), dataNode.getLeaves()); processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE); @@ -99,6 +106,7 @@ public class CpsDataServiceImpl implements CpsDataService { final String parentNodeXpath, final String dataNodeUpdatesAsJson, final OffsetDateTime observedTimestamp) { + CpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection<DataNode> dataNodeUpdates = buildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodeUpdatesAsJson); @@ -109,9 +117,31 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override + public String startSession() { + return cpsDataPersistenceService.startSession(); + } + + @Override + public void closeSession(final String sessionId) { + cpsDataPersistenceService.closeSession(sessionId); + } + + @Override + public void lockAnchor(final String sessionID, final String dataspaceName, final String anchorName) { + lockAnchor(sessionID, dataspaceName, anchorName, DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS); + } + + @Override + public void lockAnchor(final String sessionID, final String dataspaceName, + final String anchorName, final Long timeoutInMilliseconds) { + cpsDataPersistenceService.lockAnchor(sessionID, dataspaceName, anchorName, timeoutInMilliseconds); + } + + @Override public void replaceNodeTree(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { - final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData); + CpsValidator.validateNameCharacters(dataspaceName, anchorName); + final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData); cpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName, dataNode); processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE); } @@ -119,6 +149,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); @@ -127,6 +158,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); } @@ -134,6 +166,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); } @@ -141,7 +174,8 @@ public class CpsDataServiceImpl implements CpsDataService { @Override public void deleteDataNodes(final String dataspaceName, final String anchorName, final OffsetDateTime observedTimestamp) { - final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); + CpsValidator.validateNameCharacters(dataspaceName, anchorName); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName); processDataUpdatedEventAsync(anchor, ROOT_NODE_XPATH, Operation.DELETE, observedTimestamp); } @@ -149,6 +183,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); } @@ -156,8 +191,8 @@ public class CpsDataServiceImpl implements CpsDataService { private DataNode buildDataNode(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String jsonData) { - final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); - final var schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName()); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); + final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName()); if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { final NormalizedNode<?, ?> normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext); @@ -176,8 +211,8 @@ public class CpsDataServiceImpl implements CpsDataService { final String parentNodeXpath, final String jsonData) { - final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); - final var schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName()); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); + final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName()); final NormalizedNode<?, ?> normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath); final Collection<DataNode> dataNodes = new DataNodeBuilder() @@ -194,7 +229,7 @@ public class CpsDataServiceImpl implements CpsDataService { private void processDataUpdatedEventAsync(final String dataspaceName, final String anchorName, final OffsetDateTime observedTimestamp, final String xpath, final Operation operation) { - final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); this.processDataUpdatedEventAsync(anchor, xpath, operation, observedTimestamp); } 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..db8a81f276 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,23 +94,25 @@ 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, anchorName); return cpsModulePersistenceService.getYangResourceModuleReferences(dataspaceName, anchorName); } - private boolean isCascadeDeleteProhibited(final CascadeDeleteAllowed cascadeDeleteAllowed) { - return CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED == cascadeDeleteAllowed; - } - @Override public Collection<ModuleReference> identifyNewModuleReferences( final Collection<ModuleReference> moduleReferencesToCheck) { return cpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck); } + private boolean isCascadeDeleteProhibited(final CascadeDeleteAllowed cascadeDeleteAllowed) { + return CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED == cascadeDeleteAllowed; + } + } 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/spi/CpsAdminPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java index dd4059d88c..25167e844a 100755 --- a/cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java +++ b/cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.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 * ================================================================================ @@ -23,8 +23,10 @@ package org.onap.cps.spi; import java.util.Collection; +import java.util.Set; import org.onap.cps.spi.exceptions.AlreadyDefinedException; import org.onap.cps.spi.model.Anchor; +import org.onap.cps.spi.model.CmHandleQueryParameters; /* Service for handling CPS admin data. @@ -99,4 +101,12 @@ public interface CpsAdminPersistenceService { * @param anchorName anchor name */ void deleteAnchor(String dataspaceName, String anchorName); + + /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryParameters the cm handle query parameters + * @return collection of cm handle ids + */ + Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters); } diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java index fd658861c2..43cfffee70 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java +++ b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020 Nordix Foundation. + * Copyright (C) 2020-2022 Nordix Foundation. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ @@ -148,4 +148,29 @@ public interface CpsDataPersistenceService { Collection<DataNode> queryDataNodes(String dataspaceName, String anchorName, String cpsPath, FetchDescendantsOption fetchDescendantsOption); + /** + * Starts a session which allows use of locks and batch interaction with the persistence service. + * + * @return Session ID string + */ + String startSession(); + + /** + * Close session. + * + * @param sessionId session ID + */ + void closeSession(String sessionId); + + /** + * Lock anchor. + * To release locks(s), the session holding the lock(s) must be closed. + * + * @param sessionID session ID + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param timeoutInMilliseconds lock attempt timeout in milliseconds + */ + void lockAnchor(String sessionID, String dataspaceName, String anchorName, Long timeoutInMilliseconds); + } diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionManagerException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionManagerException.java new file mode 100644 index 0000000000..4000bfc51d --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionManagerException.java @@ -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.spi.exceptions; + + +public class SessionManagerException extends CpsException { + + private static final long serialVersionUID = 7957090904519019500L; + + /** + * Constructor. + * + * @param message the error message + * @param details the error details + * @param cause the cause of the exception + */ + public SessionManagerException(final String message, final String details, final Throwable cause) { + super(message, details, cause); + } + + /** + * Constructor. + * + * @param message the error message + * @param details the error details + */ + public SessionManagerException(final String message, final String details) { + super(message, details); + } +} diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionTimeoutException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionTimeoutException.java new file mode 100644 index 0000000000..92b4aa7a8b --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionTimeoutException.java @@ -0,0 +1,31 @@ +/* + * ============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.spi.exceptions; + +@SuppressWarnings("squid:S110") // Team agreed to accept 6 levels of inheritance for CPS Exceptions +public class SessionTimeoutException extends SessionManagerException { + + private static final long serialVersionUID = -8809577494038691360L; + + public SessionTimeoutException(final String message, final String details, final Throwable cause) { + super(message, details, cause); + } +} diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java b/cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java new file mode 100644 index 0000000000..ff4e627636 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java @@ -0,0 +1,41 @@ +/* + * ============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.spi.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.Map; +import javax.validation.Valid; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@JsonInclude(Include.NON_NULL) +public class CmHandleQueryParameters { + + @JsonProperty("publicCmHandleProperties") + @Valid + private Map<String, String> publicProperties = Collections.emptyMap(); + +} diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java b/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java index 55e7b9970b..43aa06b81b 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java +++ b/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java @@ -26,11 +26,13 @@ import java.util.Collection; import java.util.Collections; import java.util.Map; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @Setter(AccessLevel.PROTECTED) @Getter +@EqualsAndHashCode public class DataNode { DataNode() { } 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..28b49c9666 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java @@ -0,0 +1,62 @@ +/* + * ============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 java.util.regex.Pattern; +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(); + private static final Pattern TOPIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9]([._-](?![._-])|" + + "[a-zA-Z0-9]){0,120}[a-zA-Z0-9]$"); + + /** + * 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)); + } + } + } + } + + /** + * Validate kafka topic name pattern. + * @param topicName name of the topic to be validated + */ + public static boolean validateTopicName(final String topicName) { + return topicName != null && TOPIC_NAME_PATTERN.matcher(topicName).matches(); + } +} diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy index bb122d1ae2..33868ccf06 100755 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy @@ -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 * ================================================================================ @@ -24,7 +24,9 @@ package org.onap.cps.api.impl import org.onap.cps.api.CpsDataService import org.onap.cps.spi.CpsAdminPersistenceService +import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.model.Anchor +import org.onap.cps.spi.model.CmHandleQueryParameters import spock.lang.Specification import java.time.OffsetDateTime @@ -40,6 +42,15 @@ class CpsAdminServiceImplSpec extends Specification { 1 * mockCpsAdminPersistenceService.createDataspace('someDataspace') } + def 'Create a dataspace with an invalid dataspace name.'() { + when: 'create dataspace method is invoked with incorrectly named dataspace' + objectUnderTest.createDataspace('Dataspace Name with spaces') + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsAdminPersistenceService.createDataspace(_) + } + def 'Create anchor method invokes persistence service.'() { when: 'create anchor method is invoked' objectUnderTest.createAnchor('someDataspace', 'someSchemaSet', 'someAnchorName') @@ -47,6 +58,15 @@ class CpsAdminServiceImplSpec extends Specification { 1 * mockCpsAdminPersistenceService.createAnchor('someDataspace', 'someSchemaSet', 'someAnchorName') } + def 'Create an anchor with an invalid anchor name.'() { + when: 'create anchor method is invoked with incorrectly named dataspace' + objectUnderTest.createAnchor('someDataspace', 'someSchemaSet', 'Anchor Name With Spaces') + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsAdminPersistenceService.createAnchor(_, _, _) + } + def 'Retrieve all anchors for dataspace.'() { given: 'that anchor is associated with the dataspace' def anchors = [new Anchor()] @@ -55,6 +75,15 @@ class CpsAdminServiceImplSpec extends Specification { objectUnderTest.getAnchors('someDataspace') == anchors } + def 'Retrieve all anchors with an invalid dataspace name.'() { + when: 'get anchors is invoked with an invalid dataspace name' + objectUnderTest.getAnchors('Dataspace name with spaces') + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'cps admin persistence get anchors is not invoked' + 0 * mockCpsAdminPersistenceService.getAnchors(_) + } + def 'Retrieve all anchors for schema-set.'() { given: 'that anchor is associated with the dataspace and schemaset' def anchors = [new Anchor()] @@ -62,6 +91,20 @@ class CpsAdminServiceImplSpec extends Specification { expect: 'the collection provided by persistence service is returned as result' objectUnderTest.getAnchors('someDataspace', 'someSchemaSet') == anchors } + def 'Retrieve all anchors for schema-set with invalid #scenario.'() { + when: 'the collection provided by persistence service is returned as result' + objectUnderTest.getAnchors(dataspaceName, schemaSetName) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'cps admin persistence get anchors is not invoked' + 0 * mockCpsAdminPersistenceService.getAnchors(_, _) + where: 'the following parameters are used' + scenario | dataspaceName | schemaSetName + 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName' + 'schema set name' | 'dataspaceName' | 'schema set name with spaces' + 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces' + } + def 'Retrieve anchor for dataspace and provided anchor name.'() { given: 'that anchor name is associated with the dataspace' @@ -71,6 +114,20 @@ class CpsAdminServiceImplSpec extends Specification { assert objectUnderTest.getAnchor('someDataspace','someAnchor') == anchor } + def 'Retrieve anchor with invalid #scenario.'() { + when: 'get anchors is invoked with an invalid dataspace name' + objectUnderTest.getAnchor(dataspaceName, anchorName) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'cps admin persistence get anchor is not invoked' + 0 * mockCpsAdminPersistenceService.getAnchor(_, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Delete anchor.'() { when: 'delete anchor is invoked' objectUnderTest.deleteAnchor('someDataspace','someAnchor') @@ -80,6 +137,22 @@ class CpsAdminServiceImplSpec extends Specification { 1 * mockCpsAdminPersistenceService.deleteAnchor('someDataspace','someAnchor') } + def 'Delete anchor with invalid #scenario.'() { + when: 'delete anchor is invoked' + objectUnderTest.deleteAnchor(dataspaceName, anchorName) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'delete data nodes is invoked on the data service with expected parameters' + 0 * mockCpsDataService.deleteDataNodes(_,_, _ as OffsetDateTime ) + and: 'the persistence service method is invoked with same parameters to delete anchor' + 0 * mockCpsAdminPersistenceService.deleteAnchor(_,_) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Query all anchor identifiers for a dataspace and module names.'() { given: 'the persistence service is invoked with the expected parameters and returns a list of anchors' mockCpsAdminPersistenceService.queryAnchors('some-dataspace-name', ['some-module-name']) >> [new Anchor(name:'some-anchor-identifier')] @@ -88,6 +161,15 @@ class CpsAdminServiceImplSpec extends Specification { } + def 'Query all anchor identifiers for a dataspace and module names with an invalid dataspace name.'() { + when: 'delete anchor is invoked' + objectUnderTest.queryAnchorNames('some dataspace name', _ as Collection<String>) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'delete data nodes is not invoked' + 0 * mockCpsAdminPersistenceService.queryAnchors(_, _) + } + def 'Delete dataspace.'() { when: 'delete dataspace is invoked' objectUnderTest.deleteDataspace('someDataspace') @@ -95,4 +177,22 @@ class CpsAdminServiceImplSpec extends Specification { 1 * mockCpsAdminPersistenceService.deleteDataspace('someDataspace') } + def 'Query CM Handles.'() { + given: 'a cm handle query' + def cmHandleQueryParameters = new CmHandleQueryParameters() + when: 'query cm handles is invoked' + objectUnderTest.queryCmHandles(cmHandleQueryParameters) + then: 'associated persistence service method is invoked with correct parameter' + 1 * mockCpsAdminPersistenceService.queryCmHandles(cmHandleQueryParameters) + } + + def 'Delete dataspace with invalid dataspace id.'() { + when: 'delete dataspace is invoked' + objectUnderTest.deleteDataspace('some dataspace name') + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'associated persistence service method is not invoked' + 0 * mockCpsAdminPersistenceService.deleteDataspace(_) + } + } 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 785788be90..8b9d545295 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 @@ -30,6 +30,7 @@ import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.FetchDescendantsOption 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.yang.YangTextSchemaSourceSet import org.onap.cps.yang.YangTextSchemaSourceSetBuilder @@ -50,9 +51,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() @@ -69,6 +70,22 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/', Operation.CREATE) } + def 'Saving json data with invalid #scenario.'() { + when: 'save data method is invoked with invalid #scenario' + objectUnderTest.saveData(dataspaceName, anchorName, _ as String, observedTimestamp) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsDataPersistenceService.storeDataNode(_, _, _) + and: 'data updated event is not sent to notification service' + 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Saving child data fragment under existing node.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') @@ -82,6 +99,22 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/test-tree', Operation.CREATE) } + def 'Saving child data fragment under existing node with invalid #scenario.'() { + when: 'save data method is invoked with test-tree and an invalid #scenario' + objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', _ as String, observedTimestamp) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsDataPersistenceService.addChildDataNode(_, _, _,_) + and: 'data updated event is not sent to notification service' + 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Saving list element data fragment under existing node.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') @@ -112,6 +145,20 @@ class CpsDataServiceImplSpec extends Specification { thrown(DataValidationException) } + def 'Saving list element data fragment with invalid #scenario.'() { + when: 'save data method is invoked with an invalid #scenario' + objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', _ as String, observedTimestamp) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'add list elements persistence method is not invoked' + 0 * mockCpsDataPersistenceService.addListElements(_, _, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Get data node with option #fetchDescendantsOption.'() { def xpath = '/xpath' def dataNode = new DataNodeBuilder().withXpath(xpath).build() @@ -123,6 +170,20 @@ class CpsDataServiceImplSpec extends Specification { fetchDescendantsOption << FetchDescendantsOption.values() } + def 'Get data node with option invalid #scenario.'() { + when: 'get data node is invoked with #scenario' + objectUnderTest.getDataNode(dataspaceName, anchorName, '/test-tree', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'get data node persistence service is not invoked' + 0 * mockCpsDataPersistenceService.getDataNode(_, _, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Update data node leaves: #scenario.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') @@ -138,6 +199,22 @@ class CpsDataServiceImplSpec extends Specification { 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']' | ['name': 'Name'] } + def 'Update data node with invalid #scenario.'() { + when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath' + objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/', '{"test-tree": {"branch": []}}', observedTimestamp) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsDataPersistenceService.updateDataLeaves(_, _, _, _) + and: 'data updated event is not sent to notification service' + 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Update list-element data node with : #scenario.'() { given: 'schema set for given anchor and dataspace references bookstore model' setupSchemaSetMocks('bookstore.yang') @@ -167,6 +244,24 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/bookstore', Operation.UPDATE) } + def 'Update Bookstore node leaves with invalid #scenario' () { + when: 'update data method is invoked with an invalid #scenario' + objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, + '/bookstore', _ as String, observedTimestamp) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsDataPersistenceService.updateDataLeaves(_, _, _, _) + and: 'the data updated event is not sent to the notification service' + 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + + def 'Replace data node: #scenario.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') @@ -183,6 +278,22 @@ class CpsDataServiceImplSpec extends Specification { 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']' } + def 'Replace data node with invalid #scenario.'() { + when: 'replace data method is invoked with invalid #scenario' + objectUnderTest.replaceNodeTree(dataspaceName, anchorName, '/', _ as String, observedTimestamp) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsDataPersistenceService.replaceDataNodeTree(_, _,_) + and: 'data updated event is not sent to notification service' + 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Replace list content data fragment under parent node.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') @@ -213,6 +324,22 @@ class CpsDataServiceImplSpec extends Specification { thrown(DataValidationException) } + def 'Replace whole list content with an invalid #scenario.'() { + when: 'replace list data method is invoked with invalid #scenario' + objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', _ as Collection<DataNode>, observedTimestamp) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsDataPersistenceService.replaceListContent(_, _,_) + and: 'data updated event is not sent to notification service' + 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Delete list element under existing node.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') @@ -224,6 +351,23 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/test-tree/branch', Operation.DELETE) } + + def 'Delete list element with an invalid #scenario.'() { + when: 'delete list data method is invoked with with invalid #scenario' + objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsDataPersistenceService.deleteListDataNode(_, _, _) + and: 'data updated event is not sent to notification service' + 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Delete data node under anchor and dataspace.'() { given: 'schema set for given anchor and dataspace references test tree model' setupSchemaSetMocks('test-tree.yang') @@ -235,6 +379,22 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/data-node', Operation.DELETE) } + def 'Delete data node with an invalid #scenario.'() { + when: 'delete data node method is invoked with invalid #scenario' + objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsDataPersistenceService.deleteDataNode(_, _, _) + and: 'data updated event is not sent to notification service' + 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Delete all data nodes for a given anchor and dataspace.'() { given: 'schema set for given anchor and dataspace references test tree model' setupSchemaSetMocks('test-tree.yang') @@ -254,4 +414,37 @@ class CpsDataServiceImplSpec extends Specification { def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext } + + def 'start session'() { + when: 'start session method is called' + objectUnderTest.startSession() + then: 'the persistence service method to start session is invoked' + 1 * mockCpsDataPersistenceService.startSession() + } + + def 'close session'(){ + given: 'session Id from calling the start session method' + def sessionId = objectUnderTest.startSession() + when: 'close session method is called' + objectUnderTest.closeSession(sessionId) + then: 'the persistence service method to close session is invoked' + 1 * mockCpsDataPersistenceService.closeSession(sessionId) + } + + def 'lock anchor with no timeout parameter'(){ + when: 'lock anchor method with no timeout parameter with details of anchor entity to lock' + objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName') + then: 'the persistence service method to lock anchor is invoked with default timeout' + 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', + 'some-anchorName', 300L) + } + + def 'lock anchor with timeout parameter'(){ + when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock' + objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', + 'some-anchorName', 250L) + then: 'the persistence service method to lock anchor is invoked with the given timeout' + 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', + 'some-anchorName', 250L) + } } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy index bae06bb9ec..95d731478f 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy @@ -24,7 +24,9 @@ package org.onap.cps.api.impl import org.onap.cps.TestUtils import org.onap.cps.api.CpsAdminService +import org.onap.cps.spi.CascadeDeleteAllowed import org.onap.cps.spi.CpsModulePersistenceService +import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.exceptions.ModelValidationException import org.onap.cps.spi.exceptions.SchemaSetInUseException import org.onap.cps.spi.model.Anchor @@ -51,6 +53,20 @@ class CpsModuleServiceImplSpec extends Specification { 1 * mockCpsModulePersistenceService.storeSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap) } + def 'Create a schema set with an invalid #scenario.'() { + when: 'create dataspace method is invoked with incorrectly named dataspace' + objectUnderTest.createSchemaSet(dataspaceName, schemaSetName, _ as Map<String, String>) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsModulePersistenceService.storeSchemaSet(_, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | schemaSetName + 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName' + 'schema set name name' | 'dataspaceName' | 'schema set name with spaces' + 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces' + } + def 'Create schema set from new modules and existing modules.'() { given: 'a list of existing modules module reference' def moduleReferenceForExistingModule = new ModuleReference("test", "2021-10-12","test.org") @@ -61,6 +77,20 @@ class CpsModuleServiceImplSpec extends Specification { 1 * mockCpsModulePersistenceService.storeSchemaSetFromModules("someDataspaceName", "someSchemaSetName", [newModule: "newContent"], listOfExistingModulesModuleReference) } + def 'Create schema set from new modules and existing modules with invalid #scenario.'() { + when: 'create dataspace method is invoked with incorrectly named dataspace' + objectUnderTest.createSchemaSetFromModules(dataspaceName, schemaSetName, _ as Map<String, String>, _ as Collection<ModuleReference>) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsModulePersistenceService.storeSchemaSetFromModules(_, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | schemaSetName + 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName' + 'schema set name name' | 'dataspaceName' | 'schema set name with spaces' + 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces' + } + def 'Create schema set from invalid resources'() { given: 'Invalid yang resource as name-to-content map' def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('invalid.yang') @@ -83,6 +113,20 @@ class CpsModuleServiceImplSpec extends Specification { result.getModuleReferences().contains(new ModuleReference('stores', '2020-09-15', 'org:onap:ccsdk:sample')) } + def 'Get a schema set with an invalid #scenario'() { + when: 'create dataspace method is invoked with incorrectly named dataspace' + objectUnderTest.getSchemaSet(dataspaceName, schemaSetName) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the yang resource cache is not invoked' + 0 * mockYangTextSchemaSourceSetCache.get(_, _) + where: 'the following parameters are used' + scenario | dataspaceName | schemaSetName + 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName' + 'schema set name' | 'dataspaceName' | 'schema set name with spaces' + 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces' + } + def 'Delete schema-set when cascade is allowed.'() { given: '#numberOfAnchors anchors are associated with schemaset' def associatedAnchors = createAnchors(numberOfAnchors) @@ -125,6 +169,26 @@ class CpsModuleServiceImplSpec extends Specification { thrown(SchemaSetInUseException) } + def 'Delete a schema set with an invalid #scenario.'() { + when: 'create dataspace method is invoked with incorrectly named dataspace' + objectUnderTest.deleteSchemaSet(dataspaceName, schemaSetName, CASCADE_DELETE_ALLOWED) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'anchor deletion is called 0 times' + 0 * mockCpsAdminService.deleteAnchor(_, _) + and: 'the delete schema set persistence service method is not invoked' + 0 * mockCpsModulePersistenceService.deleteSchemaSet(_, _, _) + and: 'schema set will be removed from the cache is not invoked' + 0 * mockYangTextSchemaSourceSetCache.removeFromCache(_, _) + and: 'orphan yang resources are deleted is not invoked' + 0 * mockCpsModulePersistenceService.deleteUnusedYangResourceModules() + where: 'the following parameters are used' + scenario | dataspaceName | schemaSetName + 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName' + 'schema set name name' | 'dataspaceName' | 'schema set name with spaces' + 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces' + } + def createAnchors(int anchorCount) { def anchors = [] (0..<anchorCount).each { anchors.add(new Anchor("my-anchor-$it", 'my-dataspace', 'my-schemaset')) } @@ -139,6 +203,15 @@ class CpsModuleServiceImplSpec extends Specification { objectUnderTest.getYangResourceModuleReferences('someDataspaceName') == moduleReferences } + def 'Get all yang resources module references given an invalid dataspace name.'() { + when: 'the get yang resources module references method is invoked with an invalid dataspace name' + objectUnderTest.getYangResourceModuleReferences('dataspace name with spaces') + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsModulePersistenceService.getYangResourceModuleReferences(_) + } + def 'Get all yang resources module references for the given dataspace name and anchor name.'() { given: 'the module store service service returns a list module references' @@ -148,6 +221,20 @@ class CpsModuleServiceImplSpec extends Specification { objectUnderTest.getYangResourcesModuleReferences('someDataspaceName', 'someAnchorName') == moduleReferences } + def 'Get all yang resources module references given an invalid #scenario.'() { + when: 'the get yang resources module references method is invoked with invalid #scenario' + objectUnderTest.getYangResourcesModuleReferences(dataspaceName, anchorName) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsModulePersistenceService.getYangResourceModuleReferences(_, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + def 'Identifying new module references'(){ given: 'module references from cm handle' def moduleReferencesToCheck = [new ModuleReference('some-module', 'some-revision')] 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..55a252c27d 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 @@ -22,6 +22,7 @@ package org.onap.cps.api.impl import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.FetchDescendantsOption +import org.onap.cps.spi.exceptions.DataValidationException import spock.lang.Specification class CpsQueryServiceImplSpec extends Specification { @@ -35,8 +36,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) @@ -45,4 +46,19 @@ class CpsQueryServiceImplSpec extends Specification { where: 'all fetch descendants options are supported' fetchDescendantsOption << FetchDescendantsOption.values() } + + def 'Query data nodes by cps path with invalid #scenario.'() { + when: 'queryDataNodes is invoked' + objectUnderTest.queryDataNodes(dataspaceName, anchorName, '/cps-path', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service is not invoked' + 0 * mockCpsDataPersistenceService.queryDataNodes(_, _, _, _) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/YangTextSchemaSourceSetCacheSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/YangTextSchemaSourceSetCacheSpec.groovy index 860b7399d2..06c675a255 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/YangTextSchemaSourceSetCacheSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/YangTextSchemaSourceSetCacheSpec.groovy @@ -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. @@ -22,6 +23,7 @@ package org.onap.cps.api.impl import org.onap.cps.TestUtils import org.onap.cps.spi.CpsModulePersistenceService +import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.yang.YangTextSchemaSourceSet import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.spockframework.spring.SpringBean @@ -88,6 +90,20 @@ class YangTextSchemaSourceSetCacheSpec extends Specification { 0 * mockModuleStoreService.getYangSchemaResources(_, _) } + def 'Cache Hit: with invalid #scenario'() { + when: 'schema-set information is asked' + objectUnderTest.get(dataspaceName, schemaSetName) + then: 'an data validation exception is thrown' + thrown(DataValidationException) + and: 'module persistence is not invoked' + 0 * mockModuleStoreService.getYangSchemaResources(_, _) + where: 'the following parameters are used' + scenario | dataspaceName | schemaSetName + 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName' + 'schema set name' | 'dataspaceName' | 'schema set name with spaces' + 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces' + } + def 'Cache Update: when no data exist in the cache'() { given: 'a schema set exists' def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang') @@ -99,7 +115,24 @@ class YangTextSchemaSourceSetCacheSpec extends Specification { cachedValue.getModuleReferences() == yangTextSchemaSourceSet.getModuleReferences() } - def 'Cache Evict: remove when exist'() { + def 'Cache Update: with invalid #scenario'() { + given: 'a schema set exists' + def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang') + def yangTextSchemaSourceSet = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap) + when: 'schema-set information is asked' + objectUnderTest.updateCache(dataspaceName, schemaSetName, yangTextSchemaSourceSet) + then: 'an data validation exception is thrown' + thrown(DataValidationException) + and: 'module persistence is not invoked' + 0 * mockModuleStoreService.getYangSchemaResources(_, _) + where: 'the following parameters are used' + scenario | dataspaceName | schemaSetName + 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName' + 'schema set name' | 'dataspaceName' | 'schema set name with spaces' + 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces' + } + + def 'Cache Evict:with invalid #scenario'() { given: 'a schema set exists in cache' def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang') def yangTextSchemaSourceSet = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap) @@ -112,6 +145,18 @@ class YangTextSchemaSourceSetCacheSpec extends Specification { assert getCachedValue('my-dataspace', 'my-schemaset') == null } + def 'Cache Evict: remove when exist'() { + when: 'cache is evicted for schemaset' + objectUnderTest.removeFromCache(dataspaceName, schemaSetName) + then: 'an data validation exception is thrown' + thrown(DataValidationException) + where: 'the following parameters are used' + scenario | dataspaceName | schemaSetName + 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName' + 'schema set name' | 'dataspaceName' | 'schema set name with spaces' + 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces' + } + def 'Cache Evict: remove when does not exist'() { given: 'cache is empty' yangResourceCacheImpl.clear() 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..ce728ef1c1 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy @@ -0,0 +1,62 @@ +/* + * ============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' + } + + def 'Validating topic names.'() { + when: 'the topic name is validated' + def isValidTopicName = CpsValidator.validateTopicName(topicName) + then: 'boolean response will be returned for #scenario' + assert isValidTopicName == booleanResponse + where: 'the following names are used' + scenario | topicName || booleanResponse + 'valid topic' | 'my-topic-name' || true + 'empty topic' | '' || false + 'blank topic' | ' ' || false + 'null topic' | null || false + 'invalid non empty topic' | '1_5_*_#' || false + } +} diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangTextSchemaSourceSetSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy index b6250612ed..236221aca7 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/YangTextSchemaSourceSetSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2020-2021 Pantheon.tech - * Modifications Copyright (C) 2020-2021 Nordix Foundation + * Modifications Copyright (C) 2020-2022 Nordix Foundation * Modifications Copyright (C) 2021 Bell Canada. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,22 +20,24 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.utils +package org.onap.cps.yang + import org.onap.cps.TestUtils import org.onap.cps.spi.exceptions.ModelValidationException -import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.opendaylight.yangtools.yang.common.Revision import spock.lang.Specification -class YangTextSchemaSourceSetSpec extends Specification { +class YangTextSchemaSourceSetBuilderSpec extends Specification { def 'Building a valid YangTextSchemaSourceSet using #filenameCase filename.'() { given: 'a yang model (file)' def yangResourceNameToContent = [filename: TestUtils.getResourceFileContent('bookstore.yang')] when: 'the content is parsed' def result = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - then: 'the result contains 1 module of the correct name and revision' + then: 'it can be validated successfully' + YangTextSchemaSourceSetBuilder.validate(yangResourceNameToContent) + and: 'the result contains 1 module of the correct name and revision' result.modules.size() == 1 def optionalModule = result.findModule('stores', Revision.of('2020-09-15')) optionalModule.isPresent() diff --git a/csit/data/cmHandleRegistration.json b/csit/data/cmHandleRegistration.json deleted file mode 100644 index 0133148fda..0000000000 --- a/csit/data/cmHandleRegistration.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "cmHandles": [ - "PNFDemo" - ] -}
\ No newline at end of file diff --git a/csit/plans/cps/test.properties b/csit/plans/cps/test.properties index 53b7d40d0b..47bb43b0a1 100644 --- a/csit/plans/cps/test.properties +++ b/csit/plans/cps/test.properties @@ -23,4 +23,4 @@ DMI_SERVICE_URL=http://$LOCAL_IP:$DMI_PORT DOCKER_REPO=nexus3.onap.org:10003 CPS_VERSION=latest -DMI_VERSION=1.1.0-SNAPSHOT-latest
\ No newline at end of file +DMI_VERSION=1.2.0-SNAPSHOT-latest
\ No newline at end of file diff --git a/csit/plans/cps/testplan.txt b/csit/plans/cps/testplan.txt index 8069bb72e7..d4615e7010 100644 --- a/csit/plans/cps/testplan.txt +++ b/csit/plans/cps/testplan.txt @@ -1,5 +1,5 @@ # ============LICENSE_START======================================================= -# Copyright (C) 2021 Nordix Foundation +# 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. @@ -17,8 +17,8 @@ # Test suites are relative paths under csit/tests/. # Place the suites in run order. actuator -cps-model-sync -ncmp-passthrough cps-admin cps-data - +cps-model-sync +ncmp-passthrough +public-properties-query
\ No newline at end of file diff --git a/csit/tests/cps-model-sync/cps-model-sync.robot b/csit/tests/cps-model-sync/cps-model-sync.robot index dfad948614..ea082b5a89 100644 --- a/csit/tests/cps-model-sync/cps-model-sync.robot +++ b/csit/tests/cps-model-sync/cps-model-sync.robot @@ -34,7 +34,7 @@ ${auth} Basic Y3BzdXNlcjpjcHNyMGNrcyE= ${ncmpInventoryBasePath} /ncmpInventory ${ncmpBasePath} /ncmp ${dmiUrl} http://${DMI_HOST}:${DMI_PORT} -${jsonDataCreate} {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","createdCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Sci-Fi Book"},"publicCmHandleProperties":{"Contact":"storeemail@bookstore.com"}}]} +${jsonDataCreate} {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","createdCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Sci-Fi Book"},"publicCmHandleProperties":{"Contact":"storeemail@bookstore.com", "Contact2":"storeemail2@bookstore.com"}}]} ${jsonDataUpdate} {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","updatedCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Romance Book"},"publicCmHandleProperties":{"Contact":"newemailforstore@bookstore.com"}}]} *** Test Cases *** @@ -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/csit/tests/public-properties-query/public-properties-query.robot b/csit/tests/public-properties-query/public-properties-query.robot new file mode 100644 index 0000000000..3a640871b9 --- /dev/null +++ b/csit/tests/public-properties-query/public-properties-query.robot @@ -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========================================================= + */ + +*** Settings *** +Documentation Public Properties Query Test + +Library Collections +Library OperatingSystem +Library RequestsLibrary +Library BuiltIn + +Suite Setup Create Session CPS_URL http://${CPS_CORE_HOST}:${CPS_CORE_PORT} + +*** Variables *** + +${auth} Basic Y3BzdXNlcjpjcHNyMGNrcyE= +${ncmpBasePath} /ncmp/v1 +${jsonMatchingQueryParameters} {"publicCmHandleProperties": {"Contact" : "newemailforstore@bookstore.com", "Contact2" : "storeemail2@bookstore.com"}} +${jsonMissingPropertyQueryParameters} {"publicCmHandleProperties": { "" : "doesnt matter"}} + +*** Test Cases *** +Retrieve CM Handles where query parameters Match + ${uri}= Set Variable ${ncmpBasePath}/data/ch/searches + ${headers}= Create Dictionary Content-Type=application/json Authorization=${auth} + ${response}= POST On Session CPS_URL ${uri} headers=${headers} data=${jsonMatchingQueryParameters} + ${responseJson}= Set Variable ${response.json()} + Should Be Equal As Strings ${response.status_code} 200 + Should Contain ${responseJson} PNFDemo + +Throw 400 when Structure of Request is Incorrect + ${uri}= Set Variable ${ncmpBasePath}/data/ch/searches + ${headers}= Create Dictionary Content-Type=application/json Authorization=${auth} + ${response}= POST On Session CPS_URL ${uri} headers=${headers} data=${jsonMissingPropertyQueryParameters} expected_status=400 + Should Be Equal As Strings ${response} <Response [400]> diff --git a/docs/admin-guide.rst b/docs/admin-guide.rst index 203151bf8f..1bc7f4ff38 100644 --- a/docs/admin-guide.rst +++ b/docs/admin-guide.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 .. _adminGuide: @@ -49,8 +49,6 @@ CPS Log pattern Change logging level -------------------- -.. container:: ulist - - Curl command 1. Check current log level of "logging.level.org.onap.cps" if it is set to it's default value (INFO) .. code-block:: java @@ -193,3 +191,19 @@ Prometheus Metrics can be checked at the following endpoint .. code:: http://<cps-component-service-name>:8081/manage/prometheus + +Naming Validation +----------------- + +As part of the Jakarta 3.1.0 release, CPS has added validation to the names of the following components: + + - Dataspace names + - Schema Set names + - Anchor names + - Cm-Handle identifiers + +The following characters along with spaces are no longer valid for naming of these components. + +.. code:: + + !"#$%&'()*+,./\:;<=>?@[]^`{|}~ diff --git a/docs/api/swagger/cps/openapi.yaml b/docs/api/swagger/cps/openapi.yaml index 2fc8d7f338..983252f5af 100644 --- a/docs/api/swagger/cps/openapi.yaml +++ b/docs/api/swagger/cps/openapi.yaml @@ -15,27 +15,28 @@ info: x-logo: url: cps_logo.png servers: - - url: /cps/api +- url: /cps/api tags: - - name: cps-admin - description: cps Admin - - name: cps-data - description: cps Data +- name: cps-admin + description: cps Admin +- name: cps-data + description: cps Data paths: /v1/dataspaces: post: tags: - - cps-admin + - cps-admin summary: Create a dataspace description: Create a new dataspace operationId: createDataspace parameters: - - name: dataspace-name - in: query - description: dataspace-name - required: true - schema: - type: string + - name: dataspace-name + in: query + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace responses: "201": description: Created @@ -43,38 +44,130 @@ paths: text/plain: schema: type: string + example: my-resource + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized + "403": + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred + delete: + tags: + - cps-admin + summary: Delete a dataspace + description: Delete a dataspace + operationId: deleteDataspace + parameters: + - name: dataspace-name + in: query + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + responses: + "204": + description: No Content + content: {} "400": description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred /v1/dataspaces/{dataspace-name}/anchors: get: tags: - - cps-admin + - cps-admin summary: Get anchors description: "Read all anchors, given a dataspace" operationId: getAnchors parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace responses: "200": description: OK @@ -90,49 +183,68 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "404": - description: The specified resource was not found + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred post: tags: - - cps-admin + - cps-admin summary: Create an anchor description: Create a new anchor in the given dataspace operationId: createAnchor parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: schema-set-name - in: query - description: schema-set-name - required: true - schema: - type: string - - name: anchor-name - in: query - description: anchor-name - required: true - schema: - type: string + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: schema-set-name + in: query + description: schema-set-name + required: true + schema: + type: string + example: my-schema-set + - name: anchor-name + in: query + description: anchor-name + required: true + schema: + type: string + example: my-anchor responses: "201": description: Created @@ -140,44 +252,79 @@ paths: text/plain: schema: type: string + example: my-resource "400": description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}: get: tags: - - cps-admin + - cps-admin summary: Get an anchor description: Read an anchor given an anchor name and a dataspace operationId: getAnchor parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: anchor-name - in: path - description: anchor-name - required: true - schema: - type: string + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: anchor-name + in: path + description: anchor-name + required: true + schema: + type: string + example: my-anchor responses: "200": description: OK @@ -191,43 +338,61 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "404": - description: The specified resource was not found + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred delete: tags: - - cps-admin + - cps-admin summary: Delete an anchor description: Delete an anchor given an anchor name and a dataspace operationId: deleteAnchor parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: anchor-name - in: path - description: anchor-name - required: true - schema: - type: string + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: anchor-name + in: path + description: anchor-name + required: true + schema: + type: string + example: my-anchor responses: "204": description: No Content @@ -238,38 +403,62 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred /v1/dataspaces/{dataspace-name}/schema-sets: post: tags: - - cps-admin + - cps-admin summary: Create a schema set description: Create a new schema set in the given dataspace operationId: createSchemaSet parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: schema-set-name - in: query - description: schema-set-name - required: true - schema: - type: string + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: schema-set-name + in: query + description: schema-set-name + required: true + schema: + type: string + example: my-schema-set requestBody: content: multipart/form-data: @@ -283,44 +472,79 @@ paths: text/plain: schema: type: string + example: my-resource "400": description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred /v1/dataspaces/{dataspace-name}/schema-sets/{schema-set-name}: get: tags: - - cps-admin + - cps-admin summary: Get a schema set description: Read a schema set given a schema set name and a dataspace operationId: getSchemaSet parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: schema-set-name - in: path - description: schema-set-name - required: true - schema: - type: string + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: schema-set-name + in: path + description: schema-set-name + required: true + schema: + type: string + example: my-schema-set responses: "200": description: OK @@ -334,43 +558,61 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "404": - description: The specified resource was not found + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred delete: tags: - - cps-admin + - cps-admin summary: Delete a schema set description: Delete a schema set given a schema set name and a dataspace operationId: deleteSchemaSet parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: schema-set-name - in: path - description: schema-set-name - required: true - schema: - type: string + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: schema-set-name + in: path + description: schema-set-name + required: true + schema: + type: string + example: my-schema-set responses: "204": description: No Content @@ -381,59 +623,93 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden "409": description: Conflict content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/node: get: tags: - - cps-data + - cps-data summary: Get a node description: Get a node with an option to retrieve all the children for a given anchor and dataspace operationId: getNodeByDataspaceAndAnchor parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: anchor-name - in: path - description: anchor-name - required: true - schema: - type: string - - name: xpath - in: query - description: xpath - required: false - schema: - type: string - default: / - - name: include-descendants - in: query - description: include-descendants - required: false - schema: - type: boolean - default: false + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: anchor-name + in: path + description: anchor-name + required: true + schema: + type: string + example: my-anchor + - name: xpath + in: query + description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html" + required: false + schema: + type: string + default: / + examples: + container xpath: + value: /shops/bookstore + list attributes xpath: + value: "/shops/bookstore/categories[@code=1]" + - name: include-descendants + in: query + description: include-descendants + required: false + schema: + type: boolean + example: false + default: false responses: "200": description: OK @@ -441,75 +717,100 @@ paths: application/json: schema: type: object - example: - child: my_child - leafList: "leafListElement1, leafListElement2" - leaf: my_leaf + examples: + dataSample: + $ref: '#/components/examples/dataSample' "400": description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "404": - description: The specified resource was not found + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred x-codegen-request-body-name: xpath /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes: put: tags: - - cps-data + - cps-data summary: Replace a node with descendants description: "Replace a node with descendants for a given dataspace, anchor\ \ and a parent node xpath" operationId: replaceNode parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: anchor-name - in: path - description: anchor-name - required: true - schema: - type: string - - name: xpath - in: query - description: xpath - required: false - schema: - type: string - default: / - - name: observed-timestamp - in: query - description: observed-timestamp - required: false - schema: - type: string - example: 2021-03-21T00:10:34.030-0100 + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: anchor-name + in: path + description: anchor-name + required: true + schema: + type: string + example: my-anchor + - name: xpath + in: query + description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html" + required: false + schema: + type: string + default: / + examples: + container xpath: + value: /shops/bookstore + list attributes xpath: + value: "/shops/bookstore/categories[@code=1]" + - name: observed-timestamp + in: query + description: observed-timestamp + required: false + schema: + type: string + example: 2021-03-21T00:10:34.030-0100 requestBody: content: application/json: schema: - type: string + type: object + examples: + dataSample: + $ref: '#/components/examples/dataSample' required: true responses: "200": @@ -518,64 +819,97 @@ paths: application/json: schema: type: object - example: - key: value + examples: + dataSample: + value: "" "400": description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred post: tags: - - cps-data + - cps-data summary: Create a node description: Create a node for a given anchor and dataspace operationId: createNode parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: anchor-name - in: path - description: anchor-name - required: true - schema: - type: string - - name: xpath - in: query - description: xpath - required: false - schema: - type: string - default: / - - name: observed-timestamp - in: query - description: observed-timestamp - required: false - schema: - type: string - example: 2021-03-21T00:10:34.030-0100 + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: anchor-name + in: path + description: anchor-name + required: true + schema: + type: string + example: my-anchor + - name: xpath + in: query + description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html" + required: false + schema: + type: string + default: / + examples: + container xpath: + value: /shops/bookstore + list attributes xpath: + value: "/shops/bookstore/categories[@code=1]" + - name: observed-timestamp + in: query + description: observed-timestamp + required: false + schema: + type: string + example: 2021-03-21T00:10:34.030-0100 requestBody: content: application/json: schema: - type: string + type: object + examples: + dataSample: + $ref: '#/components/examples/dataSample' required: true responses: "201": @@ -584,63 +918,191 @@ paths: text/plain: schema: type: string + example: my-resource + "400": + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized + "403": + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred + delete: + tags: + - cps-data + summary: Delete a data node + description: Delete a datanode for a given dataspace and anchor given a node + xpath. + operationId: deleteDataNode + parameters: + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: anchor-name + in: path + description: anchor-name + required: true + schema: + type: string + example: my-anchor + - name: xpath + in: query + description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html" + required: false + schema: + type: string + default: / + examples: + container xpath: + value: /shops/bookstore + list attributes xpath: + value: "/shops/bookstore/categories[@code=1]" + - name: observed-timestamp + in: query + description: observed-timestamp + required: false + schema: + type: string + example: 2021-03-21T00:10:34.030-0100 + responses: + "204": + description: No Content + content: {} "400": description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred patch: tags: - - cps-data + - cps-data summary: Update node leaves description: Update a data node leaves for a given dataspace and anchor and a parent node xpath operationId: updateNodeLeaves parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: anchor-name - in: path - description: anchor-name - required: true - schema: - type: string - - name: xpath - in: query - description: xpath - required: false - schema: - type: string - default: / - - name: observed-timestamp - in: query - description: observed-timestamp - required: false - schema: - type: string - example: 2021-03-21T00:10:34.030-0100 + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: anchor-name + in: path + description: anchor-name + required: true + schema: + type: string + example: my-anchor + - name: xpath + in: query + description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html" + required: false + schema: + type: string + default: / + examples: + container xpath: + value: /shops/bookstore + list attributes xpath: + value: "/shops/bookstore/categories[@code=1]" + - name: observed-timestamp + in: query + description: observed-timestamp + required: false + schema: + type: string + example: 2021-03-21T00:10:34.030-0100 requestBody: content: application/json: schema: - type: string + type: object + examples: + dataSample: + $ref: '#/components/examples/dataSample' required: true responses: "200": @@ -649,129 +1111,195 @@ paths: application/json: schema: type: object - example: - key: value + examples: + dataSample: + value: "" "400": description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/list-nodes: put: tags: - - cps-data - summary: Replace list-node child element(s) under existing parent node - description: Replace list-node child elements under existing node for a given - anchor and dataspace - operationId: replaceListNodeElements + - cps-data + summary: Replace list content + description: "Replace list content under a given parent, anchor and dataspace" + operationId: replaceListContent parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: anchor-name - in: path - description: anchor-name - required: true - schema: - type: string - - name: xpath - in: query - description: xpath - required: true - schema: - type: string - - name: observed-timestamp - in: query - description: observed-timestamp - required: false - schema: - type: string - example: 2021-03-21T00:10:34.030-0100 + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: anchor-name + in: path + description: anchor-name + required: true + schema: + type: string + example: my-anchor + - name: xpath + in: query + description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html" + required: true + schema: + type: string + examples: + container xpath: + value: /shops/bookstore + list attributes xpath: + value: "/shops/bookstore/categories[@code=1]" + - name: observed-timestamp + in: query + description: observed-timestamp + required: false + schema: + type: string + example: 2021-03-21T00:10:34.030-0100 requestBody: content: application/json: schema: - type: string + type: object + examples: + dataSample: + $ref: '#/components/examples/dataSample' required: true responses: "200": - description: Created + description: OK content: - text/plain: + application/json: schema: - type: string + type: object + examples: + dataSample: + value: "" "400": description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred post: tags: - - cps-data - summary: Add list-node child element(s) under existing parent node - description: Add list-node child elements to existing node for a given anchor - and dataspace - operationId: addListNodeElements + - cps-data + summary: Add list element(s) + description: Add list element(s) to a list for a given anchor and dataspace + operationId: addListElements parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: anchor-name - in: path - description: anchor-name - required: true - schema: - type: string - - name: xpath - in: query - description: xpath - required: true - schema: - type: string - - name: observed-timestamp - in: query - description: observed-timestamp - required: false - schema: - type: string - example: 2021-03-21T00:10:34.030-0100 + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: anchor-name + in: path + description: anchor-name + required: true + schema: + type: string + example: my-anchor + - name: xpath + in: query + description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html" + required: true + schema: + type: string + examples: + container xpath: + value: /shops/bookstore + list attributes xpath: + value: "/shops/bookstore/categories[@code=1]" + - name: observed-timestamp + in: query + description: observed-timestamp + required: false + schema: + type: string + example: 2021-03-21T00:10:34.030-0100 requestBody: content: application/json: schema: - type: string + type: object + examples: + dataSample: + $ref: '#/components/examples/dataSample' required: true responses: "201": @@ -780,57 +1308,86 @@ paths: text/plain: schema: type: string + example: my-resource "400": description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred delete: tags: - - cps-data - summary: Delete list-node child element(s) under existing parent node - description: Delete list-node child elements under existing node for a given - anchor and dataspace - operationId: deleteListNodeElements + - cps-data + summary: Delete one or all list element(s) + description: Delete one or all list element(s) for a given anchor and dataspace + operationId: deleteListOrListElement parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: anchor-name - in: path - description: anchor-name - required: true - schema: - type: string - - name: xpath - in: query - description: xpath - required: true - schema: - type: string - - name: observed-timestamp - in: query - description: observed-timestamp - required: false - schema: - type: string - example: 2021-03-21T00:10:34.030-0100 + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: anchor-name + in: path + description: anchor-name + required: true + schema: + type: string + example: my-anchor + - name: xpath + in: query + description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html" + required: true + schema: + type: string + examples: + container xpath: + value: /shops/bookstore + list attributes xpath: + value: "/shops/bookstore/categories[@code=1]" + - name: observed-timestamp + in: query + description: observed-timestamp + required: false + schema: + type: string + example: 2021-03-21T00:10:34.030-0100 responses: "204": description: No Content @@ -841,52 +1398,83 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred + deprecated: true /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query: get: tags: - - cps-query + - cps-query summary: Query data nodes description: Query data nodes for the given dataspace and anchor using CPS path operationId: getNodesByDataspaceAndAnchorAndCpsPath parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - - name: anchor-name - in: path - description: anchor-name - required: true - schema: - type: string - - name: cps-path - in: query - description: cps-path - required: false - schema: - type: string - default: / - - name: include-descendants - in: query - description: include-descendants - required: false - schema: - type: boolean - default: false + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: anchor-name + in: path + description: anchor-name + required: true + schema: + type: string + example: my-anchor + - name: cps-path + in: query + description: "For more details on cps path, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html" + required: false + schema: + type: string + default: / + examples: + container cps path: + value: //bookstore + list attributes cps path: + value: "//categories[@code=1]" + - name: include-descendants + in: query + description: include-descendants + required: false + schema: + type: boolean + example: false + default: false responses: "200": description: OK @@ -894,32 +1482,49 @@ paths: application/json: schema: type: object - example: - key: value + examples: + dataSample: + $ref: '#/components/examples/dataSample' "400": description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid "401": description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized "403": description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "404": - description: The specified resource was not found + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "500": + description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred x-codegen-request-body-name: xpath components: schemas: @@ -929,29 +1534,26 @@ components: properties: status: type: string - example: "400" message: type: string - example: Dataspace not found details: type: string - example: Dataspace with name D1 does not exist. AnchorDetails: title: Anchor details by anchor Name type: object properties: name: type: string - example: my_anchor + example: my-anchor dataspaceName: type: string - example: my_dataspace + example: my-dataspace schemaSetName: type: string - example: my_schema_set + example: my-schema-set MultipartFile: required: - - file + - file type: object properties: file: @@ -960,28 +1562,40 @@ components: format: binary SchemaSetDetails: title: Schema set details by dataspace and schemasetName + required: + - moduleReferences type: object properties: dataspaceName: type: string - example: my_dataspace + example: my-dataspace moduleReferences: type: array items: $ref: '#/components/schemas/ModuleReferences' name: type: string - example: my_schema_set + example: my-schema-set ModuleReferences: title: Module reference object type: object properties: name: type: string - example: module_reference_name + example: my-module-reference-name namespace: type: string - example: module_reference_namespace + example: my-module-reference-namespace revision: type: string - example: module_reference_revision + example: my-module-reference-revision + examples: + dataSample: + value: + test:bookstore: + bookstore-name: Chapters + categories: + - code: 1 + name: SciFi + - code: 2 + name: kids diff --git a/docs/api/swagger/ncmp/openapi-inventory.yaml b/docs/api/swagger/ncmp/openapi-inventory.yaml index 154a4411da..30896f6068 100644 --- a/docs/api/swagger/ncmp/openapi-inventory.yaml +++ b/docs/api/swagger/ncmp/openapi-inventory.yaml @@ -86,23 +86,16 @@ components: $ref: '#/components/schemas/RestInputCmHandle' updatedCmHandles: type: array - example: - cmHandle: my-cm-handle - cmHandleProperties: - add-my-property: add-property - update-my-property: updated-property - delete-my-property: ~ - publicCmHandleProperties: - add-my-property: add-property - update-my-property: updated-property - delete-my-property: ~ items: $ref: '#/components/schemas/RestInputCmHandle' removedCmHandles: type: array + example: + - my-cm-handle1 + - my-cm-handle2 + - my-cm-handle3 items: type: string - example: "[\"my-cm-handle1\",\"my-cm-handle2\",\"my-cm-handle3\"]" RestInputCmHandle: required: - cmHandle diff --git a/docs/api/swagger/ncmp/openapi.yaml b/docs/api/swagger/ncmp/openapi.yaml index b7a65632e7..a43190bccd 100644 --- a/docs/api/swagger/ncmp/openapi.yaml +++ b/docs/api/swagger/ncmp/openapi.yaml @@ -41,16 +41,6 @@ paths: sample 3: value: resourceIdentifier: "parent=shops,child=bookstore" - - name: Accept - in: header - description: "Accept parameter for response, if accept parameter is null,\ - \ that means client can accept any format." - required: false - schema: - type: string - enum: - - application/json - - application/yang-data+json - name: options in: query description: "options parameter in query, it is mandatory to wrap key(s)=value(s)\ @@ -70,6 +60,17 @@ paths: sample 3: value: options: "(depth=2,fields=book/authors)" + - name: topic + in: query + description: topic parameter in query. + required: false + allowReserved: true + schema: + type: string + examples: + sample 1: + value: + topic: my-topic-name responses: "200": description: OK @@ -120,6 +121,18 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred + "502": + description: Bad Gateway + content: + application/json: + schema: + $ref: '#/components/schemas/DmiErrorMessage' + example: + message: "Bad Gateway Error Message NCMP" + dmi-response: + http-code: 400 + body: Bad Request + /v1/ch/{cm-handle}/data/ds/ncmp-datastore:passthrough-running: get: tags: @@ -155,16 +168,6 @@ paths: sample 3: value: resourceIdentifier: "parent=shops,child=bookstore" - - name: Accept - in: header - description: "Accept parameter for response, if accept parameter is null,\ - \ that means client can accept any format." - required: false - schema: - type: string - enum: - - application/json - - application/yang-data+json - name: options in: query description: "options parameter in query, it is mandatory to wrap key(s)=value(s)\ @@ -184,6 +187,17 @@ paths: sample 3: value: options: "(depth=2,fields=book/authors)" + - name: topic + in: query + description: topic parameter in query. + required: false + allowReserved: true + schema: + type: string + examples: + sample 1: + value: + topic: my-topic-name responses: "200": description: OK @@ -234,6 +248,17 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred + "502": + description: Bad Gateway + content: + application/json: + schema: + $ref: '#/components/schemas/DmiErrorMessage' + example: + message: "Bad Gateway Error Message NCMP" + dmi-response: + http-code: 400 + body: Bad Request put: tags: - network-cm-proxy @@ -340,6 +365,17 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred + "502": + description: Bad Gateway + content: + application/json: + schema: + $ref: '#/components/schemas/DmiErrorMessage' + example: + message: "Bad Gateway Error Message NCMP" + dmi-response: + http-code: 400 + body: Bad Request post: tags: - network-cm-proxy @@ -442,6 +478,17 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred + "502": + description: Bad Gateway + content: + application/json: + schema: + $ref: '#/components/schemas/DmiErrorMessage' + example: + message: "Bad Gateway Error Message NCMP" + dmi-response: + http-code: 400 + body: Bad Request delete: tags: - network-cm-proxy @@ -539,6 +586,17 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred + "502": + description: Bad Gateway + content: + application/json: + schema: + $ref: '#/components/schemas/DmiErrorMessage' + example: + message: "Bad Gateway Error Message NCMP" + dmi-response: + http-code: 400 + body: Bad Request patch: tags: - network-cm-proxy @@ -639,6 +697,17 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred + "502": + description: Bad Gateway + content: + application/json: + schema: + $ref: '#/components/schemas/DmiErrorMessage' + example: + message: "Bad Gateway Error Message NCMP" + dmi-response: + http-code: 400 + body: Bad Request /v1/ch/{cm-handle}/modules: get: tags: @@ -664,7 +733,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/ModuleReference' + $ref: '#/components/schemas/RestModuleReference' "400": description: Bad Request content: @@ -851,7 +920,24 @@ components: type: string details: type: string - ModuleReference: + # DMI Server Exception Schema + DmiErrorMessage: + title: DMI Error Message + type: object + properties: + message: + type: string + example: "Bad Gateway Error Message NCMP" + dmi-response: + type: object + properties: + http-code: + type: integer + example: 400 + body: + type: string + example: Bad Request + RestModuleReference: title: Module reference details type: object properties: 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/deployment.rst b/docs/deployment.rst index 6f450c1230..06e1ddcc08 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -7,13 +7,13 @@ .. _deployment: CPS Deployment -============== +############## .. contents:: :depth: 2 CPS OOM Charts --------------- +============== The CPS kubernetes chart is located in the `OOM repository <https://github.com/onap/oom/tree/master/kubernetes/cps>`_. This chart includes different cps components referred as <cps-component-name> further in the document are listed below: @@ -26,7 +26,8 @@ This chart includes different cps components referred as <cps-component-name> fu Please refer to the `OOM documentation <https://docs.onap.org/projects/onap-oom/en/latest/oom_user_guide.html>`_ on how to install and deploy ONAP. Installing or Upgrading CPS Components --------------------------------------- +====================================== + The assumption is you have cloned the charts from the OOM repository into a local directory. **Step 1** Go to the cps charts and edit properties in values.yaml files to make any changes to particular cps component if required. @@ -91,7 +92,7 @@ After deploying cps, keep monitoring the cps pods until they come up. kubectl get pods -n <namespace> | grep <cps-component-name> Restarting a faulty component ------------------------------ +============================= Each cps component can be restarted independently by issuing the following command: .. code-block:: bash @@ -102,7 +103,7 @@ Each cps component can be restarted independently by issuing the following comma .. _cps_common_credentials_retrieval: Credentials Retrieval ---------------------- +===================== Application and database credentials are kept in Kubernetes secrets. They are defined as external secrets in the values.yaml file to be used across different components as : @@ -161,8 +162,9 @@ Additional Cps-Core Customizations ================================== The following table lists some properties that can be specified as Helm chart -values to configure the application to be deployed. This list is not -exhaustive. +values to configure the application to be deployed. This list is not exhaustive. + +Any spring supported property can be configured by providing in ``config.additional.<spring-supported-property-name>: value`` Example: config.additional.spring.datasource.hikari.maximumPoolSize: 30 +---------------------------------------+---------------------------------------------------------------------------------------------------------+-------------------------------+ | Property | Description | Default Value | @@ -280,6 +282,10 @@ exhaustive. | notification.async.executor. | | | | thread-name-prefix | | | +---------------------------------------+---------------------------------------------------------------------------------------------------------+-------------------------------+ +| config.additional. | Specifies number of database connections between database and application. | ``10`` | +| spring.datasource.hikari. | This property controls the maximum size that the pool is allowed to reach, | | +| maximumPoolSize | including both idle and in-use connections. | | ++---------------------------------------+---------------------------------------------------------------------------------------------------------+-------------------------------+ CPS-Core Docker Installation ============================ diff --git a/docs/index.rst b/docs/index.rst index 62ba5e8b17..eaf36466f4 100755 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,7 +9,10 @@ .. _cps-framework-doc: CPS Documentation ------------------ +################# + +CPS Core +======== .. toctree:: :maxdepth: 1 @@ -22,12 +25,12 @@ CPS Documentation deployment.rst release-notes.rst -DMI-Plugin Documentation ------------------------- +DMI-Plugin +========== * :ref:`DMI-Plugin<onap-cps-ncmp-dmi-plugin:master_index>` -CPS-Temporal Documentation --------------------------- +CPS Temporal +============ * :ref:`CPS-Temporal<onap-cps-cps-temporal:master_index>` diff --git a/docs/overview.rst b/docs/overview.rst index 4b69dd8109..cde6f6db62 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -4,7 +4,7 @@ .. _overview: CPS Overview -============ +############ The Configuration Persistence Service (CPS) is a platform component that is designed to serve as a data repository for runtime data that needs persistence. @@ -28,10 +28,10 @@ Types of data that is stored: configuration and operational parameters depending on how they are used. CPS Components --------------- +============== CPS-Core -######## +-------- This is the component of CPS which encompasses the generic storage of Yang module data. **NCMP** @@ -43,13 +43,13 @@ NCMP accesses all network Data-Model-Inventory (DMI) information via NCMP-DMI-Pl even though CPS-Core could be deployed without the NCMP extension. NCMP-DMI-Plugin -#################### +--------------- The Data-Model-Inventory (DMI) Plugin is a rest interface used to synchronize CM-Handles data between CPS and DMI through the DMI-Plugin. This is built previously from the CPS-NF-Proxy component. CPS-Temporal -############ +------------ This service is responsible to provide a time oriented perspective for operational network data. It provides features to store and retrieve sequences @@ -57,14 +57,8 @@ of configurations or states along with the associated times when they occurred or have been observed. CPS Project ------------ - -Wiki: `Configuration Persistence Service Project <https://wiki.onap.org/display/DW/Configuration+Persistence+Service+Project>`_ - -Contact Information -------------------- - -onap-discuss@lists.onap.org +=========== -Meeting details `Join <https://zoom.us/j/836561560?pwd=TTZNcFhXTWYxMmZ4SlgzcVZZQXluUT09>`_ -`Agenda <https://wiki.onap.org/pages/viewpage.action?pageId=111117075>`_ +* Wiki: `Configuration Persistence Service Project <https://wiki.onap.org/display/DW/Configuration+Persistence+Service+Project>`_ +* Contact Information: onap-discuss@lists.onap.org +* Meeting details: `Join <https://zoom.us/j/836561560?pwd=TTZNcFhXTWYxMmZ4SlgzcVZZQXluUT09>`_ & `Agenda <https://wiki.onap.org/pages/viewpage.action?pageId=111117075>`_ diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 0ca0547fad..a584b580a1 100755 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -5,11 +5,8 @@ .. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING .. _release_notes: - - -================= CPS Release Notes -================= +################# .. contents:: :depth: 2 @@ -19,10 +16,45 @@ CPS Release Notes .. * * * JAKARTA * * * .. ======================== -Version: 3.0.0-SNAPSHOT -======================= +Version: 3.1.0 +============== ++--------------------------------------+--------------------------------------------------------+ +| **CPS Project** | | +| | | ++--------------------------------------+--------------------------------------------------------+ +| **Docker images** | onap/cps-and-ncmp:3.1.0 | +| | | ++--------------------------------------+--------------------------------------------------------+ +| **Release designation** | 3.1.0 Jakarta | +| | | ++--------------------------------------+--------------------------------------------------------+ +| **Release date** | | +| | | ++--------------------------------------+--------------------------------------------------------+ + +Features +-------- + - `CPS-322 <https://jira.onap.org/browse/CPS-322>`_ Implement additional validation for names and identifiers + +Version: 3.0.0 +============== + +Release Data +------------ -This section lists the main changes & fixes merged into master (snapshot) version of CPS-NCMP. This information is here to assist developers that want experiment/test using our latest code bases directly. Stability of this is not guaranteed. ++--------------------------------------+--------------------------------------------------------+ +| **CPS Project** | | +| | | ++--------------------------------------+--------------------------------------------------------+ +| **Docker images** | onap/cps-and-ncmp:3.0.0 | +| | | ++--------------------------------------+--------------------------------------------------------+ +| **Release designation** | 3.0.0 Jakarta | +| | | ++--------------------------------------+--------------------------------------------------------+ +| **Release date** | 2022 March 15 | +| | | ++--------------------------------------+--------------------------------------------------------+ Features -------- @@ -33,6 +65,8 @@ Features - `CPS-741 <https://jira.onap.org/browse/CPS-741>`_ Re sync after removing cm handles - `CPS-777 <https://jira.onap.org/browse/CPS-777>`_ Ensure all DMI operations use POST method - `CPS-780 <https://jira.onap.org/browse/CPS-780>`_ Add examples for parameters, request and response in openapi yaml for cps-core + - `CPS-789 <https://jira.onap.org/browse/CPS-789>`_ CPS Data Updated Event Schema V2 to support delete operation + - `CPS-791 <https://jira.onap.org/browse/CPS-791>`_ CPS-Core sends delete notification event - `CPS-817 <https://jira.onap.org/browse/CPS-817>`_ Create Endpoint For Get Cm Handles (incl. public properties) By Name - `CPS-837 <https://jira.onap.org/browse/CPS-837>`_ Add Remove and Update properties (DMI and Public) as part of CM Handle Registration update @@ -59,6 +93,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 diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 5a3d2f17f4..3b3441a807 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1 +1,3 @@ -lfdocs-conf
\ No newline at end of file +lfdocs-conf +sphinx>=4.2.0 # BSD +sphinx-rtd-theme>=1.0.0 # MIT diff --git a/jacoco-report/pom.xml b/jacoco-report/pom.xml index d42d89ab0b..d1181d367c 100644 --- a/jacoco-report/pom.xml +++ b/jacoco-report/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>org.onap.cps</groupId> <artifactId>cps-parent</artifactId> - <version>3.0.0-SNAPSHOT</version> + <version>3.1.0-SNAPSHOT</version> <relativePath>../cps-parent/pom.xml</relativePath> </parent> <modelVersion>4.0.0</modelVersion> @@ -32,7 +32,7 @@ <groupId>org.onap.cps</groupId>
<artifactId>cps-aggregator</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>cps</name>
diff --git a/releases/3.0.0-container.yaml b/releases/3.0.0-container.yaml new file mode 100644 index 0000000000..f227bdbf53 --- /dev/null +++ b/releases/3.0.0-container.yaml @@ -0,0 +1,8 @@ +distribution_type: container +container_release_tag: 3.0.0 +project: cps +log_dir: cps-maven-docker-stage-master/504/ +ref: a1129b696f3197fc7d8a3b63bcd84b5b2dd8874e +containers: + - name: 'cps-and-ncmp' + version: '3.0.0-20220315T180237Z' diff --git a/releases/3.0.0.yaml b/releases/3.0.0.yaml new file mode 100644 index 0000000000..60dd8116d0 --- /dev/null +++ b/releases/3.0.0.yaml @@ -0,0 +1,4 @@ +distribution_type: maven +log_dir: cps-maven-stage-master/504/ +project: cps +version: 3.0.0 diff --git a/spotbugs/pom.xml b/spotbugs/pom.xml index 50cef487e0..df033a3f4a 100644 --- a/spotbugs/pom.xml +++ b/spotbugs/pom.xml @@ -25,7 +25,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>org.onap.cps</groupId> <artifactId>spotbugs</artifactId> - <version>3.0.0-SNAPSHOT</version> + <version>3.1.0-SNAPSHOT</version> <properties> <nexusproxy>https://nexus.onap.org</nexusproxy> @@ -33,6 +33,18 @@ <snapshotNexusPath>/content/repositories/snapshots/</snapshotNexusPath> </properties> + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-deploy-plugin</artifactId> + <version>2.8.2</version> + </plugin> + </plugins> + </pluginManagement> + </build> + <distributionManagement> <repository> <id>ecomp-releases</id> diff --git a/version.properties b/version.properties index 17f2daa6cf..870b994b72 100755 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ # ============LICENSE_START======================================================= -# Copyright (C) 2021 Nordix Foundation +# Copyright (C) 2021-2022 Nordix Foundation # Modifications Copyright (C) 2022 Bell Canada. # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,7 +21,7 @@ # because they are used in Jenkins, whose plug-in doesn't support this major=3 -minor=0 +minor=1 patch=0 base_version=${major}.${minor}.${patch} |