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:
<!--
============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.
<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>
<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>
--- /dev/null
+# ============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
--- /dev/null
+============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=========================================================
--- /dev/null
+file path
+*checkstyle/*
+*.json
+*.yang
+*.rst
+*.csv
\ No newline at end of file
--- /dev/null
+email,signature
+@est.tech,Nordix Foundation
+@bell.ca,Bell Canada
\ No newline at end of file
--- /dev/null
+# ============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()
<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>
<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>
<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>
============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.
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>
<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>
<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>0.18.0</version>
<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>
<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>
# ============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.
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
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:
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
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
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"
schema:
$ref: 'components.yaml#/components/schemas/RestDmiPluginRegistration'
responses:
- 204:
+ 200:
$ref: 'components.yaml#/components/responses/NoContent'
400:
$ref: 'components.yaml#/components/responses/BadRequest'
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"
+ }
+ ]
# ============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.
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:
$ref: 'components.yaml#/components/responses/Forbidden'
500:
$ref: 'components.yaml#/components/responses/InternalServerError'
+ 502:
+ $ref: 'components.yaml#/components/responses/BadGateway'
resourceDataForPassthroughRunning:
get:
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:
$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
$ref: 'components.yaml#/components/responses/Forbidden'
500:
$ref: 'components.yaml#/components/responses/InternalServerError'
+ 502:
+ $ref: 'components.yaml#/components/responses/BadGateway'
put:
tags:
$ref: 'components.yaml#/components/responses/Forbidden'
500:
$ref: 'components.yaml#/components/responses/InternalServerError'
+ 502:
+ $ref: 'components.yaml#/components/responses/BadGateway'
patch:
tags:
$ref: 'components.yaml#/components/responses/Forbidden'
500:
$ref: 'components.yaml#/components/responses/InternalServerError'
+ 502:
+ $ref: 'components.yaml#/components/responses/BadGateway'
delete:
tags:
$ref: 'components.yaml#/components/responses/NotFound'
500:
$ref: 'components.yaml#/components/responses/InternalServerError'
+ 502:
+ $ref: 'components.yaml#/components/responses/BadGateway'
fetchModuleReferencesByCmHandle:
get:
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:
$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'
<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>
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);
* 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.
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;
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;
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;
*
* @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
@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);
}
/**
*
* @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
@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
return ResponseEntity.ok(cmHandles);
}
+ /**
+ * 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
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;
+ }
+
}
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021 Bell Canada
+ * Copyright (C) 2021-2022 Bell Canada
* Modifications Copyright (C) 2022 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
package org.onap.cps.ncmp.rest.controller;
+import java.util.List;
+import java.util.stream.Collectors;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status;
+import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse;
import org.onap.cps.ncmp.rest.api.NetworkCmProxyInventoryApi;
+import org.onap.cps.ncmp.rest.model.CmHandlerRegistrationErrorResponse;
+import org.onap.cps.ncmp.rest.model.DmiPluginRegistrationErrorResponse;
import org.onap.cps.ncmp.rest.model.RestDmiPluginRegistration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
/**
* 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());
}
}
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;
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) {
} 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);
+ }
}
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
* 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.
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)
@Shared
def NO_TOPIC = null
+ def NO_REQUEST_ID = null
def 'Get Resource Data from pass-through operational.'() {
given: 'resource data url'
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.'() {
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()
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',
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',
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'
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" +
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'
+ }
+
}
/*
* ============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");
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
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)
@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'
.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'
'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'
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)
+ }
+
}
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
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
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 }
"dmiModelPlugin":"service3",
"createdCmHandles":[
{
- "cmHandle":"ch1(new)",
+ "cmHandle":"ch1-new",
"cmHandleProperties":{
"dmiProp1":"ch1-dmi1",
"dmiProp2":"ch1-dmi2"
}
},
{
- "cmHandle":"ch2(new)",
+ "cmHandle":"ch2-new",
"cmHandleProperties":{
"dmiProp1":"ch2-dmi1",
"dmiProp2":"ch2-dmi2"
],
"updatedCmHandles":[
{
- "cmHandle":"ch3(upd)",
+ "cmHandle":"ch3-upd",
"cmHandleProperties":{
"dmiProp1":"ch3-dmi1"
},
"dmiPlugin": "service1",
"updatedCmHandles":[
{
- "cmHandle":"ch3(upd)",
+ "cmHandle":"ch3-upd",
"cmHandleProperties":{
"dmiProp1":"ch3-dmi1",
"dmiProp2":null
"dmiModelPlugin":"service3",
"createdCmHandles":[
{
- "cmHandle": "ch1(new)"
+ "cmHandle": "ch1-new"
}
]
}
<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>
* 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.
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;
* 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
*
* @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
*
* @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
*/
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);
}
* 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.
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;
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;
private final DmiDataOperations dmiDataOperations;
- private final DmiModelOperations dmiModelOperations;
-
private final CpsModuleService cpsModuleService;
private final CpsAdminService cpsAdminService;
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
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);
}
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
/*
* ============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.
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
*
* @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) {
/*
* ============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.
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);
}
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);
- }
}
--- /dev/null
+/*
+ * ============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;
+ }
+}
/*
* ============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.
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;
* @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
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()
.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);
}
/**
final OperationEnum operation,
final String requestData,
final String dataType) {
+ CpsValidator.validateNameCharacters(cmHandleId);
final YangModelCmHandle yangModelCmHandle =
yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId);
final DmiRequestBody dmiRequestBody = DmiRequestBody.builder()
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);
}
}
/*
* ============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.
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;
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,
/*
* ============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.
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
.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;
- }
}
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;
/**
* @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")),
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;
.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;
}
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;
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.
@Getter
@Setter
@NoArgsConstructor
+@JsonInclude(Include.NON_NULL)
public class YangModelCmHandle {
private String id;
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);
+++ /dev/null
-/*
- * ============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);
- }
-}
--- /dev/null
+/*
+ * ============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;
+ }
+
+}
--- /dev/null
+/*
+ * ============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();
+
+}
--- /dev/null
+/*
+ * ============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
--- /dev/null
+/*
+ * ============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
@NoArgsConstructor
public class NcmpServiceCmHandle {
- private String cmHandleID;
+ private String cmHandleId;
@JsonSetter(nulls = Nulls.AS_EMPTY)
private Map<String, String> dmiProperties = Collections.emptyMap();
/*
* ============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.
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
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']
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)
'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)
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))
+ }
}
* ============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.
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
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
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']"
>> { 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',
'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.'() {
'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',
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.'() {
mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
'testResourceId',
OPTIONS_PARAM,
- 'testAcceptParam',
PASSTHROUGH_OPERATIONAL,
NO_REQUEST_ID,
NO_TOPIC)
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')
}
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',
mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
'testResourceId',
OPTIONS_PARAM,
- 'testAcceptParam',
PASSTHROUGH_RUNNING,
NO_REQUEST_ID,
NO_TOPIC)
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.'() {
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',
'{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')
}
}
/*
* ============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.
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
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'
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'
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'
'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) {
}))
return properties
}
+
}
/*
* ============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.
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
}
/*
* ============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.
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'
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'
/*
* ============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.
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)
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'
[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)'
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'
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) {
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
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'
'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
+ }
}
/*
* ============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.
* ============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'
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'),
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) {
--- /dev/null
+/*
+ * ============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
+ }
+
+}
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'
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:
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'
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'
<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>
<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>
<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>
<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
+ <version>4.9.2</version>
<executions>
<execution>
<goals>
/*
* ============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.
grammar CpsPath ;
-cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? ancestorAxis? ;
+cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? ancestorAxis? invalidPostFix?;
ancestorAxis : SLASH KW_ANCESTOR COLONCOLON ancestorPath ;
leafName : QName ;
+invalidPostFix : (AT | CB | COLONCOLON | EQ ).+ ;
+
/*
* Lexer Rules
* Most of the lexer rules below are inspired by
/*
* ============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.
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;
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
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
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;
}
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("'");
+ }
}
/*
* ============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.
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;
* @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);
}
/**
--- /dev/null
+/*
+ * ============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;
+ }
+}
--- /dev/null
+/*
+ * ============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;
+ }
+}
/*
* ============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.
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.'() {
'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)
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'
'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.'() {
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.'() {
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
}
-
}
responses:
'201':
$ref: 'components.yml#/components/responses/Created'
+ '400':
+ $ref: 'components.yml#/components/responses/BadRequest'
'401':
$ref: 'components.yml#/components/responses/Unauthorized'
'403':
<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>
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
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(
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 }
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
<parent>\r
<groupId>org.onap.cps</groupId>\r
<artifactId>cps-parent</artifactId>\r
- <version>3.0.0-SNAPSHOT</version>\r
+ <version>3.1.0-SNAPSHOT</version>\r
<relativePath>../cps-parent/pom.xml</relativePath>\r
</parent>\r
\r
import java.util.Collection;
import java.util.List;
+import java.util.Set;
import java.util.stream.Collectors;
import javax.transaction.Transactional;
import lombok.AllArgsConstructor;
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;
private final AnchorRepository anchorRepository;
private final SchemaSetRepository schemaSetRepository;
private final YangResourceRepository yangResourceRepository;
+ private final ModuleReferenceRepository moduleReferenceRepository;
@Override
public void createDataspace(final String dataspaceName) {
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);
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;
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;
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}))\\]$");
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);
}
}
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 =
.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<>();
}
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;
}
}
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);
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;
/**
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);
+
}
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);
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(
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,");
+ " 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) {
--- /dev/null
+/*
+ * ============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;
+ }
+}
--- /dev/null
+/*
+ * ============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);
+ }
+}
--- /dev/null
+<?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
/*
* ============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
* ================================================================================
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
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
@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()
@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)
@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)
}
'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
+ }
}
/*
* ============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.
* ================================================================================
}
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])
}
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])
}
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])
}
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' || []
}
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
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
}
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])
'empty xpath' | ''
}
+ @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'
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])
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])
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])
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])
'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
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])
/*
* ============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.
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
--- /dev/null
+/*
+ * ============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)
+ }
+
+}
--- /dev/null
+/*
+ * ============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')
+ }
+
+}
/*
============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");
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"}');
/*
============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.
================================================================================
*/
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'),
(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"}');
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>\r
+<!DOCTYPE hibernate-configuration PUBLIC\r
+ "-//Hibernate/Hibernate Configuration DTD 3.0//EN"\r
+ "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">\r
+\r
+<hibernate-configuration>\r
+ <session-factory>\r
+ <property name="hibernate.connection.driver_class">org.postgresql.Driver</property>\r
+ <property name="hibernate.connection.url">${DB_URL}</property>\r
+ <property name="hibernate.connection.username">${DB_USERNAME}</property>\r
+ <property name="hibernate.connection.password">${DB_PASSWORD}</property>\r
+ <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQL82Dialect</property>\r
+ <property name="show_sql">true</property>\r
+ <property name="hibernate.hbm2ddl.auto">none</property>\r
+ </session-factory>\r
+</hibernate-configuration>
\ No newline at end of file
<parent>\r
<groupId>org.onap.cps</groupId>\r
<artifactId>cps-parent</artifactId>\r
- <version>3.0.0-SNAPSHOT</version>\r
+ <version>3.1.0-SNAPSHOT</version>\r
<relativePath>../cps-parent/pom.xml</relativePath>\r
</parent>\r
\r
/*
* ============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
* ================================================================================
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.
* 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);
}
*/
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);
+
}
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;
* @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.
* @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);
/**
* @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.
* @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.
/*
* ============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.
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;
* 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);
}
/*
* ============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
* ================================================================================
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;
@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);
+ }
}
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;
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;
@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);
}
@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);
}
@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,
@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);
final String parentNodeXpath,
final String dataNodeUpdatesAsJson,
final OffsetDateTime observedTimestamp) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Collection<DataNode> dataNodeUpdates =
buildDataNodes(dataspaceName, anchorName,
parentNodeXpath, dataNodeUpdatesAsJson);
processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE);
}
+ @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);
}
@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);
@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);
}
@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);
}
@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);
}
@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);
}
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);
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()
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);
}
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;
@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);
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);
@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)
@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);
@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;
+ }
+
}
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;
@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);
}
}
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;
*/
@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);
@CanIgnoreReturnValue
public YangTextSchemaSourceSet updateCache(final String dataspaceName, final String schemaSetName,
final YangTextSchemaSourceSet yangTextSchemaSourceSet) {
+ CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
return yangTextSchemaSourceSet;
}
*/
@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
}
/*
* ============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
* ================================================================================
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.
* @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);
}
/*
* ============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
* ================================================================================
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);
+
}
--- /dev/null
+/*
+ * ============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);
+ }
+}
--- /dev/null
+/*
+ * ============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);
+ }
+}
--- /dev/null
+/*
+ * ============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();
+
+}
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() { }
--- /dev/null
+/*
+ * ============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();
+ }
+}
/*
* ============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
* ================================================================================
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
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')
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()]
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()]
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'
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')
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')]
}
+ 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')
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(_)
+ }
+
}
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
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()
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')
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')
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()
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')
'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')
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')
'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')
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')
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')
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')
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)
+ }
}
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
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")
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')
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)
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')) }
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'
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')]
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 {
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)
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'
+ }
+
}
/*
* ============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.
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
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')
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)
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()
--- /dev/null
+/*
+ * ============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
+ }
+}
/*
* ============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");
* ============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()
+++ /dev/null
-{
- "cmHandles": [
- "PNFDemo"
- ]
-}
\ No newline at end of file
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
# ============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.
# 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
${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 ***
${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
${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
--- /dev/null
+/*
+ * ============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]>
.. 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:
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
.. 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::
+
+ !"#$%&'()*+,./\:;<=>?@[]^`{|}~
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
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
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
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
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
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:
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
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
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
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":
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":
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":
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":
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
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
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:
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:
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
$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
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)\
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
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:
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)\
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
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
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
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
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
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:
schema:
type: array
items:
- $ref: '#/components/schemas/ModuleReference'
+ $ref: '#/components/schemas/RestModuleReference'
"400":
description: Bad Request
content:
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:
.. 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:
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>
</book>
</books>
</categories>
- <categories code="2" name="Kids" numberOfBooks="1">
+ <categories code=2 name="Kids" numberOfBooks="1">
<books>
<book title="Matilda" />
</books>
</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
=============
**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
---------------
**Examples**
- ``//bookstore``
- - ``//categories[@code=1]/book``
+ - ``//categories[@code='1']/books``
- ``//bookstore/categories``
**Limitations**
- ``/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).
**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.
.. _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:
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.
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
.. _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 :
==================================
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 |
| 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
============================
.. _cps-framework-doc:
CPS Documentation
------------------
+#################
+
+CPS Core
+========
.. toctree::
:maxdepth: 1
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>`
.. _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.
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**
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
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>`_
.. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
.. _release_notes:
-
-
-=================
CPS Release Notes
-=================
+#################
.. contents::
:depth: 2
.. * * * 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
--------
- `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
`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
-lfdocs-conf
\ No newline at end of file
+lfdocs-conf
+sphinx>=4.2.0 # BSD
+sphinx-rtd-theme>=1.0.0 # MIT
<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>
\r
<groupId>org.onap.cps</groupId>\r
<artifactId>cps-aggregator</artifactId>\r
- <version>3.0.0-SNAPSHOT</version>\r
+ <version>3.1.0-SNAPSHOT</version>\r
<packaging>pom</packaging>\r
\r
<name>cps</name>\r
--- /dev/null
+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'
--- /dev/null
+distribution_type: maven
+log_dir: cps-maven-stage-master/504/
+project: cps
+version: 3.0.0
<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>
<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>
# ============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");
# 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}