summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xINFO.yaml5
-rw-r--r--checkstyle/pom.xml70
-rw-r--r--checkstyle/src/main/CopyrightCheck.py262
-rw-r--r--checkstyle/src/main/resources/copyright-template.txt16
-rw-r--r--checkstyle/src/main/resources/ignore-files-config.csv6
-rw-r--r--checkstyle/src/main/resources/project-committers-config.csv3
-rw-r--r--checkstyle/src/main/test_CopyrightCheck.py441
-rwxr-xr-xcps-application/pom.xml2
-rw-r--r--cps-bom/pom.xml14
-rwxr-xr-xcps-dependencies/pom.xml40
-rw-r--r--cps-events/pom.xml2
-rw-r--r--cps-ncmp-rest/docs/openapi/components.yaml75
-rwxr-xr-xcps-ncmp-rest/docs/openapi/ncmp-inventory.yml60
-rwxr-xr-xcps-ncmp-rest/docs/openapi/ncmp.yml43
-rwxr-xr-xcps-ncmp-rest/docs/openapi/openapi.yml5
-rw-r--r--cps-ncmp-rest/pom.xml2
-rw-r--r--cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapper.java2
-rwxr-xr-xcps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java87
-rwxr-xr-xcps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java61
-rwxr-xr-xcps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java24
-rw-r--r--cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapperSpec.groovy2
-rw-r--r--cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy107
-rw-r--r--cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy87
-rw-r--r--cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy17
-rw-r--r--cps-ncmp-rest/src/test/resources/dmi_registration_all_singing_and_dancing.json6
-rw-r--r--cps-ncmp-rest/src/test/resources/dmi_registration_updates_only.json2
-rw-r--r--cps-ncmp-rest/src/test/resources/dmi_registration_without_properties.json2
-rw-r--r--cps-ncmp-service/pom.xml2
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java28
-rwxr-xr-xcps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java323
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java36
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java31
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java45
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java21
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java4
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java7
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetriever.java4
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java10
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandle.java7
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandlesList.java70
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncService.java84
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java41
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java88
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java34
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/NcmpServiceCmHandle.java2
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy417
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy214
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy72
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy13
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy8
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy10
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy2
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetrieverSpec.groovy18
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncServiceSpec.groovy (renamed from cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplModelSyncSpec.groovy)38
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy71
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/YangModelCmHandleSpec.groovy3
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy18
-rwxr-xr-xcps-parent/pom.xml4
-rw-r--r--cps-path-parser/pom.xml3
-rw-r--r--cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g46
-rw-r--r--cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java81
-rw-r--r--cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java25
-rw-r--r--cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java81
-rwxr-xr-xcps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java55
-rw-r--r--cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy80
-rw-r--r--cps-rest/docs/openapi/cpsAdmin.yml2
-rwxr-xr-xcps-rest/pom.xml2
-rwxr-xr-xcps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy7
-rw-r--r--cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy1
-rw-r--r--cps-ri/pom.xml2
-rwxr-xr-xcps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java9
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java41
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java10
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java3
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java65
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java165
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/utils/TimeLimiterProvider.java33
-rw-r--r--cps-ri/src/main/resources/hibernate.cfg.xml16
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy31
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy42
-rwxr-xr-xcps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy61
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy116
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy69
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy99
-rw-r--r--cps-ri/src/test/resources/data/cps-path-query.sql38
-rwxr-xr-xcps-ri/src/test/resources/data/fragment.sql43
-rw-r--r--cps-ri/src/test/resources/hibernate.cfg.xml16
-rw-r--r--cps-service/pom.xml2
-rwxr-xr-xcps-service/src/main/java/org/onap/cps/api/CpsAdminService.java12
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/CpsDataService.java37
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java15
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java7
-rwxr-xr-xcps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java18
-rwxr-xr-xcps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java55
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java15
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java2
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java4
-rwxr-xr-xcps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java12
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java27
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionManagerException.java48
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionTimeoutException.java31
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java41
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java2
-rw-r--r--cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java62
-rwxr-xr-xcps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy102
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy199
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy87
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy20
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/YangTextSchemaSourceSetCacheSpec.groovy47
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy62
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy (renamed from cps-service/src/test/groovy/org/onap/cps/utils/YangTextSchemaSourceSetSpec.groovy)12
-rw-r--r--csit/data/cmHandleRegistration.json5
-rw-r--r--csit/plans/cps/test.properties2
-rw-r--r--csit/plans/cps/testplan.txt8
-rw-r--r--csit/tests/cps-model-sync/cps-model-sync.robot6
-rw-r--r--csit/tests/public-properties-query/public-properties-query.robot51
-rw-r--r--docs/admin-guide.rst20
-rw-r--r--docs/api/swagger/cps/openapi.yaml1350
-rw-r--r--docs/api/swagger/ncmp/openapi-inventory.yaml15
-rw-r--r--docs/api/swagger/ncmp/openapi.yaml130
-rw-r--r--docs/cps-path.rst150
-rw-r--r--docs/deployment.rst20
-rwxr-xr-xdocs/index.rst13
-rw-r--r--docs/overview.rst24
-rwxr-xr-xdocs/release-notes.rst51
-rw-r--r--docs/requirements-docs.txt4
-rw-r--r--jacoco-report/pom.xml2
-rw-r--r--pom.xml2
-rw-r--r--releases/3.0.0-container.yaml8
-rw-r--r--releases/3.0.0.yaml4
-rw-r--r--spotbugs/pom.xml14
-rwxr-xr-xversion.properties4
132 files changed, 5756 insertions, 1416 deletions
diff --git a/INFO.yaml b/INFO.yaml
index 279ec8504..610fd0ed8 100755
--- a/INFO.yaml
+++ b/INFO.yaml
@@ -67,6 +67,11 @@ committers:
company: 'Bell Canada'
id: 'renukumari'
timezone: 'America/Toronto'
+ - name: 'Joseph Keenan'
+ email: 'joseph.keenan@est.tech'
+ company: 'Ericsson Software Technology'
+ id: 'JosephKeenan'
+ timezone: 'Europe/Dublin'
repositories:
- cps
tsc:
diff --git a/checkstyle/pom.xml b/checkstyle/pom.xml
index 07e6cf966..8d1174200 100644
--- a/checkstyle/pom.xml
+++ b/checkstyle/pom.xml
@@ -2,6 +2,7 @@
<!--
============LICENSE_START=======================================================
Copyright (C) 2020 Pantheon.tech
+ Modifications Copyright (C) 2022 Nordix Foundation
================================================================================
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -25,7 +26,32 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>checkstyle</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
+
+ <profiles>
+ <profile>
+ <id>Windows</id>
+ <activation>
+ <os>
+ <family>Windows</family>
+ </os>
+ </activation>
+ <properties>
+ <script.executor>python</script.executor>
+ </properties>
+ </profile>
+ <profile>
+ <id>unix</id>
+ <activation>
+ <os>
+ <family>unix</family>
+ </os>
+ </activation>
+ <properties>
+ <script.executor>python3</script.executor>
+ </properties>
+ </profile>
+ </profiles>
<properties>
<nexusproxy>https://nexus.onap.org</nexusproxy>
@@ -34,6 +60,48 @@
<snapshotNexusPath>/content/repositories/snapshots/</snapshotNexusPath>
</properties>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <version>2.8.2</version>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <version>1.6.0</version>
+ <executions>
+ <execution>
+ <id>copyright-check</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <executable>${script.executor}</executable>
+ <workingDirectory>../checkstyle/src/main/</workingDirectory>
+ <arguments>
+ <argument>CopyrightCheck.py</argument>
+ <argument>resources/project-committers-config.csv</argument>
+ <argument>resources/copyright-template.txt</argument>
+ <argument>resources/ignore-files-config.csv</argument>
+ </arguments>
+ <successCodes>
+ <successCode>0</successCode>
+ <successCode>1</successCode>
+ </successCodes>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
<distributionManagement>
<repository>
<id>ecomp-releases</id>
diff --git a/checkstyle/src/main/CopyrightCheck.py b/checkstyle/src/main/CopyrightCheck.py
new file mode 100644
index 000000000..8f1dbff57
--- /dev/null
+++ b/checkstyle/src/main/CopyrightCheck.py
@@ -0,0 +1,262 @@
+# ============LICENSE_START=======================================================
+# Copyright (C) 2022 Nordix Foundation
+# ================================================================================
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=========================================================
+
+import subprocess
+import csv
+import re
+import datetime
+
+#constants
+import sys
+
+COMMITTERS_CONFIG_FILE = ''
+TEMPLATE_COPYRIGHT_FILE = ''
+IGNORE_FILE = ''
+if len(sys.argv) == 4:
+ COMMITTERS_CONFIG_FILE = sys.argv[1]
+ TEMPLATE_COPYRIGHT_FILE = sys.argv[2]
+ IGNORE_FILE = sys.argv[3]
+
+BANNER = '=' * 120
+
+def main():
+ print(BANNER + '\nCopyright Check Python Script:')
+ PermissionsCheck()
+
+ committerEmailExtension = GetCommitterEmailExtension()
+ projectCommitters = ReadProjectCommittersConfigFile()
+
+ CheckCommitterInConfigFile(committerEmailExtension, projectCommitters)
+
+ alteredFiles = FindAlteredFiles()
+
+ if alteredFiles:
+ issueCounter = CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension)
+ else:
+ issueCounter = 0
+
+ print(str(issueCounter) + ' issue(s) found after '+ str(len(alteredFiles)) + ' altered file(s) checked')
+ print(BANNER)
+
+
+# Check that Script has access to command line functions to use git
+def PermissionsCheck():
+ if 'permission denied' in subprocess.run('git', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').lower():
+ print('Error, I may not have the necessary permissions. Exiting...')
+ print(BANNER)
+ sys.exit()
+ else:
+ return
+
+# Returns List of Strings of file tracked by git which have been changed/added
+def FindAlteredFiles():
+ ignoreFilePaths = GetIgnoredFiles()
+
+ #Before Stage lower case d removes deleted files
+ stream = subprocess.run('git diff --name-only --diff-filter=d', shell=True, stdout=subprocess.PIPE)
+ fileNames = stream.stdout.decode('utf-8')
+ #Staged
+ stream = subprocess.run('git diff --name-only --cached --diff-filter=d', shell=True, stdout=subprocess.PIPE)
+ fileNames += '\n' + stream.stdout.decode('utf-8')
+ #New committed
+ stream = subprocess.run('git diff --name-only HEAD^ HEAD --diff-filter=d', shell=True, stdout=subprocess.PIPE)
+ fileNames += '\n' + stream.stdout.decode('utf-8')
+
+ #String to list of strings
+ alteredFiles = fileNames.split("\n")
+
+ #Remove duplicates
+ alteredFiles = list(dict.fromkeys(alteredFiles))
+
+ #Remove blank string(s)
+ alteredFiles = list(filter(None, alteredFiles))
+
+ #Remove ignored-extensions
+ alteredFiles = list(filter(lambda fileName: not re.match("|".join(ignoreFilePaths), fileName), alteredFiles))
+
+ return alteredFiles
+
+# Get the email of the most recent committer
+def GetCommitterEmailExtension():
+ email = subprocess.run('git show -s --format=\'%ce\'', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip('\n')
+ return email[email.index('@'):]
+
+# Read the config file with names of companies and respective email extensions
+def ReadProjectCommittersConfigFile():
+ try:
+ with open(COMMITTERS_CONFIG_FILE, 'r') as file:
+ reader = csv.reader(file, delimiter=',')
+ projectCommitters = {row[0]:row[1] for row in reader}
+ projectCommitters.pop('email') #Remove csv header
+ except FileNotFoundError:
+ print('Unable to open Project Committers Config File, have the command line arguments been set?')
+ print(BANNER)
+ sys.exit()
+ return projectCommitters
+
+def CheckCommitterInConfigFile(committerEmailExtension, projectCommitters):
+ if not committerEmailExtension in projectCommitters:
+ print('Error, Committer email is not included in config file.')
+ print('If your company is new to the project please make appropriate changes to project-committers-config.csv')
+ print('for Copyright Check to work.')
+ print('Exiting...')
+ print(BANNER)
+ sys.exit()
+ else:
+ return True
+
+# Read config file with list of files to ignore
+def GetIgnoredFiles():
+ try:
+ with open(IGNORE_FILE, 'r') as file:
+ reader = csv.reader(file)
+ ignoreFilePaths = [row[0] for row in reader]
+ ignoreFilePaths.pop(0) #Remove csv header
+ ignoreFilePaths = [filePath.replace('*', '.*') for filePath in ignoreFilePaths]
+ except FileNotFoundError:
+ print('Unable to open File Ignore Config File, have the command line arguments been set?')
+ print(BANNER)
+ sys.exit()
+ return ignoreFilePaths
+
+# Read the template copyright file
+def GetCopyrightTemplate():
+ try:
+ with open(TEMPLATE_COPYRIGHT_FILE, 'r') as file:
+ copyrightTemplate = file.readlines()
+ except FileNotFoundError:
+ print('Unable to open Template Copyright File, have the command line arguments been set?')
+ print(BANNER)
+ sys.exit()
+ return copyrightTemplate
+
+def GetProjectRootDir():
+ return subprocess.run('git rev-parse --show-toplevel', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip('\n') + '/'
+
+# Get the Copyright from the altered file
+def ParseFileCopyright(fileObject):
+ global issueCounter
+ copyrightFlag = False
+ copyrightInFile = {}
+ lineNumber = 1
+ for line in fileObject:
+ if 'LICENSE_START' in line:
+ copyrightFlag = True
+ if copyrightFlag:
+ copyrightInFile[lineNumber] = line
+ if 'LICENSE_END' in line:
+ break
+ lineNumber += 1
+
+ if not copyrightFlag:
+ print(fileObject.name + ' | no copyright found')
+ return {}, {}
+
+ copyrightSignatures = {}
+ copyrightLineNumbers = list(copyrightInFile.keys())
+ #Capture signature lines after LICENSE_START line
+ for lineNumber in copyrightLineNumbers:
+ if '=' not in copyrightInFile[lineNumber]:
+ copyrightSignatures[lineNumber] = copyrightInFile[lineNumber]
+ copyrightInFile.pop(lineNumber)
+ elif 'LICENSE_START' not in copyrightInFile[lineNumber]:
+ break
+
+ return (copyrightInFile, copyrightSignatures)
+
+# Remove the Block comment syntax
+def RemoveCommentBlock(fileCopyright):
+ # Comment Characters can very depending on file # *..
+ endOfCommentsIndex = list(fileCopyright.values())[0].index('=')
+ for key in fileCopyright:
+ fileCopyright[key] = fileCopyright[key][endOfCommentsIndex:]
+ if fileCopyright[key] == '':
+ fileCopyright[key] = '\n'
+
+ return fileCopyright
+
+def CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension):
+ issueCounter = 0
+ templateCopyright = GetCopyrightTemplate() #Get Copyright Template
+ projectRootDir = GetProjectRootDir()
+
+ for fileName in alteredFiles: # Not removed files
+ try:
+ with open(projectRootDir + fileName, 'r') as fileObject:
+ (fileCopyright, fileSignatures) = ParseFileCopyright(fileObject)
+
+ #Empty dict evaluates to false
+ if fileCopyright and fileSignatures:
+ fileCopyright = RemoveCommentBlock(fileCopyright)
+ issueCounter += CheckCopyrightFormat(fileCopyright, templateCopyright, projectRootDir + fileName)
+ committerCompany = projectCommitters[committerEmailExtension]
+ issueCounter += CheckCopyrightSignature(fileSignatures, committerCompany, projectRootDir + fileName)
+ else:
+ issueCounter += 1
+
+ except FileNotFoundError:
+ issueCounter += 1
+ print('Unable to find file ' + projectRootDir + fileName)
+ return issueCounter
+
+# Check that the filecopyright matches the template copyright and print comparison
+def CheckCopyrightFormat(copyrightInFile, templateCopyright, filePath):
+ issueCounter = 0
+ errorWithComparison = ''
+ for copyrightInFileKey, templateLine in zip(copyrightInFile, templateCopyright):
+ if copyrightInFile[copyrightInFileKey] != templateLine:
+ issueCounter += 1
+ errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' read \t ' + repr(copyrightInFile[copyrightInFileKey]) + '\n'
+ errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' expected ' + repr(templateLine) + '\n'
+ if errorWithComparison != '':
+ print(errorWithComparison.rstrip('\n'))
+ return issueCounter
+
+# Check the signatures and compare with committer signature and current year
+def CheckCopyrightSignature(copyrightSignatures, committerCompany, filePath):
+ issueCounter = 0
+ errorWithSignature = ''
+ signatureExists = False #signatureExistsForCommitter
+ afterFirstLine = False #afterFirstCopyright
+ for key in copyrightSignatures:
+ if afterFirstLine and 'Modifications Copyright' not in copyrightSignatures[key]:
+ issueCounter += 1
+ errorWithSignature += filePath + ' | line ' + str(key) + ' expected Modifications Copyright\n'
+ elif not afterFirstLine and 'Copyright' not in copyrightSignatures[key]:
+ issueCounter += 1
+ errorWithSignature += filePath + ' | line ' + str(key) + ' expected Copyright\n'
+ if committerCompany in copyrightSignatures[key]:
+ signatureExists = True
+ signatureYear = int(re.findall(r'\d+', copyrightSignatures[key])[-1])
+ currentYear = datetime.date.today().year
+ if signatureYear != currentYear:
+ issueCounter += 1
+ errorWithSignature += filePath + ' | line ' + str(key) + ' update year to include ' + str(currentYear) + '\n'
+ afterFirstLine = True
+
+ if not signatureExists:
+ issueCounter += 1
+ errorWithSignature += filePath + ' | missing company name and year for ' + committerCompany
+
+ if errorWithSignature != '':
+ print(errorWithSignature.rstrip('\n'))
+
+ return issueCounter
+
+if __name__ == '__main__':
+ main() \ No newline at end of file
diff --git a/checkstyle/src/main/resources/copyright-template.txt b/checkstyle/src/main/resources/copyright-template.txt
new file mode 100644
index 000000000..205e0caac
--- /dev/null
+++ b/checkstyle/src/main/resources/copyright-template.txt
@@ -0,0 +1,16 @@
+============LICENSE_START=======================================================
+================================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+SPDX-License-Identifier: Apache-2.0
+============LICENSE_END=========================================================
diff --git a/checkstyle/src/main/resources/ignore-files-config.csv b/checkstyle/src/main/resources/ignore-files-config.csv
new file mode 100644
index 000000000..4f7394fbb
--- /dev/null
+++ b/checkstyle/src/main/resources/ignore-files-config.csv
@@ -0,0 +1,6 @@
+file path
+*checkstyle/*
+*.json
+*.yang
+*.rst
+*.csv \ No newline at end of file
diff --git a/checkstyle/src/main/resources/project-committers-config.csv b/checkstyle/src/main/resources/project-committers-config.csv
new file mode 100644
index 000000000..85ee43bda
--- /dev/null
+++ b/checkstyle/src/main/resources/project-committers-config.csv
@@ -0,0 +1,3 @@
+email,signature
+@est.tech,Nordix Foundation
+@bell.ca,Bell Canada \ No newline at end of file
diff --git a/checkstyle/src/main/test_CopyrightCheck.py b/checkstyle/src/main/test_CopyrightCheck.py
new file mode 100644
index 000000000..177f9d4a5
--- /dev/null
+++ b/checkstyle/src/main/test_CopyrightCheck.py
@@ -0,0 +1,441 @@
+# ============LICENSE_START=======================================================
+# Copyright (C) 2022 Nordix Foundation
+# ================================================================================
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=========================================================
+
+import datetime
+import sys
+import unittest
+from unittest import mock
+from unittest.mock import MagicMock
+import io
+
+import CopyrightCheck
+
+BANNER = '=' * 120
+
+def MockStdout(command):
+ mock_stdout = MagicMock()
+ mock_stdout.configure_mock(**{"stdout.decode.return_value": command})
+ return mock_stdout
+
+class TestCopyrightCheck(unittest.TestCase):
+
+ @mock.patch('subprocess.run')
+ def test_PermissionsCheckFalse(self, mock_subprocess_run):
+ mock_subprocess_run.return_value = MockStdout('Permission denied')
+
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ with self.assertRaises(SystemExit):
+ CopyrightCheck.PermissionsCheck()
+ sys.stdout = sys.__stdout__
+
+ self.assertEqual(capturedOutput.getvalue(),
+ 'Error, I may not have the necessary permissions. Exiting...\n' + BANNER + '\n')
+
+ @mock.patch('subprocess.run')
+ def test_PermissionsCheckTrue(self, mock_subprocess_run):
+ mock_subprocess_run.return_value = MockStdout(
+ 'usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]...')
+ CopyrightCheck.PermissionsCheck() # Assert no error thrown
+
+ @mock.patch('CopyrightCheck.GetIgnoredFiles')
+ @mock.patch('subprocess.run')
+ def test_FindAlteredFiles(self, mock_subprocess_run, mock_GetIgnoredFiles):
+ mock_GetIgnoredFiles.return_value = ['.*.json', 'dir/.*']
+ mock_subprocess_run.return_value = MockStdout('File1.json\nFile2.java\nFile2.java\ndir/File3.java')
+ result = CopyrightCheck.FindAlteredFiles()
+ # Duplicates, .json and files in 'dir' removed
+ self.assertEqual(result, ['File2.java'])
+
+ @mock.patch('CopyrightCheck.GetIgnoredFiles')
+ @mock.patch('subprocess.run')
+ def test_FindAlteredFilesWithNoFileChanges(self, mock_subprocess_run, mock_GetIgnoredFiles):
+ mock_GetIgnoredFiles.return_value = ['.*.json', 'dir/.*']
+ mock_subprocess_run.return_value = MockStdout('File1.json\ndir/File3.java')
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ result = CopyrightCheck.FindAlteredFiles()
+ sys.stdout = sys.__stdout__
+
+ self.assertEqual(result, [])
+ self.assertEqual(capturedOutput.getvalue(), '')
+
+ @mock.patch('subprocess.run')
+ def test_GetCommitterEmailExtension(self, mock_subprocess_run):
+ mock_subprocess_run.return_value = MockStdout('a.committer.name@address.com')
+ result = CopyrightCheck.GetCommitterEmailExtension()
+ self.assertEqual(result, '@address.com')
+
+ def test_ReadProjectCommittersConfigFile(self):
+ mock_open = mock.mock_open(read_data="email,signature\n@address.com,Company Name")
+ with mock.patch('builtins.open', mock_open):
+ result = CopyrightCheck.ReadProjectCommittersConfigFile()
+ self.assertEqual(result, {'@address.com': 'Company Name'})
+
+ @mock.patch('CopyrightCheck.open')
+ def test_ReadProjectCommittersConfigFileError(self, mock_OpenFile):
+ mock_OpenFile.side_effect = FileNotFoundError
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ with self.assertRaises(SystemExit):
+ CopyrightCheck.ReadProjectCommittersConfigFile()
+ sys.stdout = sys.__stdout__
+ expectedOutput = ('Unable to open Project Committers Config File, have the command line arguments been set?\n' +
+ BANNER + '\n')
+ self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+ def test_CheckCommitterInConfigFileTrue(self):
+ committerEmailExtension = '@address.com'
+ projectCommitters = {'@address.com': 'Company Name'}
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ result = CopyrightCheck.CheckCommitterInConfigFile(committerEmailExtension, projectCommitters)
+ sys.stdout = sys.__stdout__
+ self.assertTrue(result)
+ self.assertEqual(capturedOutput.getvalue(), "")
+
+ def test_CheckCommitterInConfigFileFalse(self):
+ committerEmailExtension = '@address.com'
+ projectCommitters = {'@anotheraddress.com': 'Another Company Name'}
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ with self.assertRaises(SystemExit):
+ CopyrightCheck.CheckCommitterInConfigFile(committerEmailExtension, projectCommitters)
+ sys.stdout = sys.__stdout__
+ expectedOutput = ('Error, Committer email is not included in config file.\n' +
+ 'If your company is new to the project please make appropriate changes to project-committers-config.csv\n' +
+ 'for Copyright Check to work.\n' +
+ 'Exiting...\n' + BANNER + '\n')
+ self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+ def test_GetIgnoredFiles(self):
+ mock_open = mock.mock_open(read_data="file path\n*checkstyle/*\n*.json")
+ with mock.patch('builtins.open', mock_open):
+ result = CopyrightCheck.GetIgnoredFiles()
+ self.assertEqual(result, [".*checkstyle/.*", ".*.json"])
+
+ @mock.patch('CopyrightCheck.open')
+ def test_GetIgnoredFilesError(self, mock_OpenFile):
+ mock_OpenFile.side_effect = FileNotFoundError
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ with self.assertRaises(SystemExit):
+ CopyrightCheck.GetIgnoredFiles()
+ sys.stdout = sys.__stdout__
+ expectedOutput = ('Unable to open File Ignore Config File, have the command line arguments been set?\n' +
+ BANNER + '\n')
+ self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+ def test_GetCopyrightTemplate(self):
+ mock_open = mock.mock_open(read_data="****\nThis is a\nCopyright File\n****")
+ with mock.patch('builtins.open', mock_open):
+ result = CopyrightCheck.GetCopyrightTemplate()
+ self.assertEqual(result, ["****\n", "This is a\n", "Copyright File\n", "****"])
+
+ @mock.patch('CopyrightCheck.open')
+ def test_GetCopyrightTemplateError(self, mock_OpenFile):
+ mock_OpenFile.side_effect = FileNotFoundError
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ with self.assertRaises(SystemExit):
+ CopyrightCheck.GetCopyrightTemplate()
+ sys.stdout = sys.__stdout__
+ expectedOutput = ('Unable to open Template Copyright File, have the command line arguments been set?\n' +
+ BANNER + '\n')
+ self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+ @mock.patch('subprocess.run')
+ def test_GetProjectRootDir(self, mock_subprocess_run):
+ mock_subprocess_run.return_value = MockStdout('project/root/dir\n')
+ result = CopyrightCheck.GetProjectRootDir()
+ self.assertEqual(result, 'project/root/dir/')
+
+
+ def test_ParseFileCopyright(self):
+ readFromFile = ["#Before lines will not be included\n",
+ "#===LICENSE_START===\n",
+ "#Copyright (C) 0000 Some Company\n",
+ "#A line without signature\n",
+ "#===============================\n",
+ "#This is the start of the Copyright\n",
+ "#===LICENSE_END===\n",
+ "After lines will not be included"]
+ copyright, signatures = CopyrightCheck.ParseFileCopyright(readFromFile)
+ self.assertEqual(copyright, {2: "#===LICENSE_START===\n",
+ 5: "#===============================\n",
+ 6: "#This is the start of the Copyright\n",
+ 7: "#===LICENSE_END===\n"})
+ self.assertEqual(signatures, {3: "#Copyright (C) 0000 Some Company\n",
+ 4: "#A line without signature\n"})
+
+ def test_ParseFileCopyrightNoCopyright(self):
+ fileObject = io.StringIO("#This is not\na copyright\n")
+ fileObject.name = 'some/file/name'
+
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ copyright, signatures = CopyrightCheck.ParseFileCopyright(fileObject)
+ sys.stdout = sys.__stdout__
+
+ self.assertEqual(copyright, {})
+ self.assertEqual(signatures, {})
+ self.assertEqual(capturedOutput.getvalue(), 'some/file/name | no copyright found\n')
+
+ def test_RemoveCommentBlock(self):
+ commentCharactersList = ['# ', '* ', '# ', '* ']
+
+ for commentCharacters in commentCharactersList:
+ copyright = {1: commentCharacters + '===LICENSE_START===\n',
+ 2: '\n',
+ 3: commentCharacters + 'This is the License\n',
+ 4: commentCharacters + '===LICENSE_END===\n'}
+ result = CopyrightCheck.RemoveCommentBlock(copyright)
+ self.assertEqual(result, {1: '===LICENSE_START===\n',
+ 2: '\n',
+ 3: 'This is the License\n',
+ 4: '===LICENSE_END===\n'})
+
+ @mock.patch('CopyrightCheck.open')
+ @mock.patch('CopyrightCheck.GetProjectRootDir')
+ @mock.patch('CopyrightCheck.GetCopyrightTemplate')
+ def test_CheckCopyrightForFileNotFound(self, mock_GetCopyrightTemplate, mock_GetProjectRootDir, mock_OpenFile):
+ mock_GetCopyrightTemplate.return_value = 'some-copyright-template'
+ mock_GetProjectRootDir.return_value = 'some/project/root/dir/'
+ mock_OpenFile.side_effect = FileNotFoundError
+
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ result = CopyrightCheck.CheckCopyrightForFiles(['some-file.java'], {}, [])
+ sys.stdout = sys.__stdout__
+
+ self.assertEqual(capturedOutput.getvalue(), 'Unable to find file some/project/root/dir/some-file.java\n')
+ self.assertEqual(result, 1)
+
+ @mock.patch('CopyrightCheck.ParseFileCopyright')
+ @mock.patch('CopyrightCheck.GetProjectRootDir')
+ @mock.patch('CopyrightCheck.GetCopyrightTemplate')
+ def test_CheckCopyrightForFileWithNoCopyright(self, mock_GetCopyrightTemplate, mock_GetProjectRootDir,
+ mock_ParseFileCopyright):
+ mock_GetCopyrightTemplate.return_value = 'some-copyright-template'
+ mock_GetProjectRootDir.return_value = 'some/project/root/dir/'
+ mock_ParseFileCopyright.return_value = ({}, {})
+ mock_open = mock.mock_open(read_data="some-file-content")
+
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ with mock.patch('builtins.open', mock_open):
+ result = CopyrightCheck.CheckCopyrightForFiles(['some-file.java'], {}, [])
+ sys.stdout = sys.__stdout__
+
+ self.assertEqual(capturedOutput.getvalue(), "")
+ self.assertEqual(result, 1)
+
+
+ @mock.patch('CopyrightCheck.CheckCopyrightSignature')
+ @mock.patch('CopyrightCheck.CheckCopyrightFormat')
+ @mock.patch('CopyrightCheck.ParseFileCopyright')
+ @mock.patch('CopyrightCheck.GetProjectRootDir')
+ @mock.patch('CopyrightCheck.GetCopyrightTemplate')
+ def test_CheckCopyrightForFilesWhichAreRight(self, mock_GetCopyrightTemplate, mock_GetProjectRootDir,
+ mock_ParseFileCopyright, mock_CheckCopyrightFormat,
+ mock_CheckCopyrightSignature):
+ mock_GetCopyrightTemplate.return_value = 'some-copyright-template'
+ mock_GetProjectRootDir.return_value = 'some/project/root/dir/'
+ mock_ParseFileCopyright.return_value = ({1: '# =some-copyright-line'}, {2: '# =some-signature-line'})
+ mock_open = mock.mock_open(read_data="# =some-file-content")
+ mock_CheckCopyrightFormat.return_value = 0
+ mock_CheckCopyrightSignature.return_value = 0
+
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ with mock.patch('builtins.open', mock_open):
+ result = CopyrightCheck.CheckCopyrightForFiles(['some-file.java', 'another-file.java'], {'@address.com': 'Some Company'}, '@address.com')
+ sys.stdout = sys.__stdout__
+ self.assertEqual(result, 0)
+ self.assertEqual(capturedOutput.getvalue(), "")
+
+ mock_GetCopyrightTemplate.assert_called_once_with()
+ mock_GetProjectRootDir.assert_called_once_with()
+ self.assertEqual(mock_ParseFileCopyright.call_count, 2)
+ mock_CheckCopyrightFormat.assert_has_calls([
+ mock.call({1: '=some-copyright-line'}, 'some-copyright-template', 'some/project/root/dir/some-file.java'),
+ mock.call({1: '=some-copyright-line'}, 'some-copyright-template', 'some/project/root/dir/another-file.java')
+ ])
+ mock_CheckCopyrightSignature.assert_has_calls([
+ mock.call({2: '# =some-signature-line'}, 'Some Company', 'some/project/root/dir/some-file.java'),
+ mock.call({2: '# =some-signature-line'}, 'Some Company', 'some/project/root/dir/another-file.java')
+ ])
+ self.assertEqual(mock_CheckCopyrightFormat.call_count, 2)
+ self.assertEqual(mock_CheckCopyrightSignature.call_count, 2)
+
+
+ def test_CheckCopyrightFormatWhichIsWrong(self):
+ fileCopyright = {1: '---LICENSE_START---\n',
+ 2: 'This is the license typo\n',
+ 3: '',
+ 4: '===license_end===\n'}
+ templateCopyright = ['===LICENSE_START===\n',
+ 'This is the license\n',
+ '\n',
+ '===LICENSE_END===\n']
+
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ result = CopyrightCheck.CheckCopyrightFormat(fileCopyright, templateCopyright, 'some/file/path')
+ sys.stdout = sys.__stdout__
+
+ expectedOutput = ("some/file/path | line 1 read \t '---LICENSE_START---\\n'\n" +
+ "some/file/path | line 1 expected '===LICENSE_START===\\n'\n" +
+ "some/file/path | line 2 read \t 'This is the license typo\\n'\n" +
+ "some/file/path | line 2 expected 'This is the license\\n'\n" +
+ "some/file/path | line 3 read \t ''\n" +
+ "some/file/path | line 3 expected '\\n'\n" +
+ "some/file/path | line 4 read \t '===license_end===\\n'\n" +
+ "some/file/path | line 4 expected '===LICENSE_END===\\n'\n")
+
+ self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+ self.assertEqual(result, 4)
+
+ def test_CheckCopyrightFormatWhichIsCorrect(self):
+ fileCopyright = {1: '===LICENSE_START===\n',
+ 2: 'This is the license\n',
+ 3: '\n',
+ 4: '===LICENSE_END===\n'}
+ templateCopyright = ['===LICENSE_START===\n',
+ 'This is the license\n',
+ '\n',
+ '===LICENSE_END===\n']
+
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ result = CopyrightCheck.CheckCopyrightFormat(fileCopyright, templateCopyright, 'some/file/path')
+ sys.stdout = sys.__stdout__
+
+ self.assertEqual(capturedOutput.getvalue(), "")
+ self.assertEqual(result, 0)
+
+ def test_CheckCopyrightSignatureWhichIsWrong(self):
+ fileSignatures = {1: "Trigger expected Copy-right",
+ 2: "Trigger expected Mod Copy-right"}
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ result = CopyrightCheck.CheckCopyrightSignature(fileSignatures, 'Some-Company', 'some/file/path')
+ sys.stdout = sys.__stdout__
+
+ expectedOutput = ("some/file/path | line 1 expected Copyright\n" +
+ "some/file/path | line 2 expected Modifications Copyright\n" +
+ "some/file/path | missing company name and year for Some-Company\n")
+
+ self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+ self.assertEqual(result, 3)
+
+ def test_CheckCopyrightSignatureWhichHasWrongYear(self):
+ currentYear = datetime.date.today().year
+ fileSignatures = {1: "Copyright (C) 1999 Some-Company"}
+
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ result = CopyrightCheck.CheckCopyrightSignature(fileSignatures, 'Some-Company', 'some/file/path')
+ sys.stdout = sys.__stdout__
+
+ self.assertEqual(capturedOutput.getvalue(),
+ "some/file/path | line 1 update year to include " + str(currentYear) + "\n")
+ self.assertEqual(result, 1)
+
+ def test_CheckCopyrightSignatureWhichIsRight(self):
+ currentYear = datetime.date.today().year
+ fileSignatures = {1: "Copyright (C) " + str(currentYear) + " Some-Company"}
+
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+ result = CopyrightCheck.CheckCopyrightSignature(fileSignatures, 'Some-Company', 'some/file/path')
+ sys.stdout = sys.__stdout__
+
+ self.assertEqual(capturedOutput.getvalue(), "")
+ self.assertEqual(result, 0)
+
+ @mock.patch('CopyrightCheck.CheckCopyrightForFiles')
+ @mock.patch('CopyrightCheck.FindAlteredFiles')
+ @mock.patch('CopyrightCheck.CheckCommitterInConfigFile')
+ @mock.patch('CopyrightCheck.ReadProjectCommittersConfigFile')
+ @mock.patch('CopyrightCheck.GetCommitterEmailExtension')
+ @mock.patch('CopyrightCheck.PermissionsCheck')
+ def test_Main(self, mock_PermissionsCheck, mock_GetCommitterEmailExtension, mock_ReadProjectCommittersConfigFile,
+ mock_CheckCommitterInConfigFile, mock_FindAlteredFiles, mock_CheckCopyrightForFiles):
+
+ mock_GetCommitterEmailExtension.return_value = '@address.com'
+ mock_ReadProjectCommittersConfigFile.return_value = {'@address.com', 'Some Company'}
+ mock_FindAlteredFiles.return_value = ['some-file.java']
+ mock_CheckCopyrightForFiles.return_value = 5
+
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+
+ CopyrightCheck.main()
+
+ sys.stdout = sys.__stdout__
+
+ expectedOutput = (BANNER + '\nCopyright Check Python Script:\n' +
+ '5 issue(s) found after 1 altered file(s) checked\n' +
+ BANNER + '\n')
+
+ self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+ mock_PermissionsCheck.assert_called_once_with()
+ mock_GetCommitterEmailExtension.assert_called_once_with()
+ mock_ReadProjectCommittersConfigFile.assert_called_once_with()
+ mock_CheckCommitterInConfigFile.assert_called_once_with('@address.com', {'@address.com', 'Some Company'})
+ mock_FindAlteredFiles.assert_called_once_with()
+ mock_CheckCopyrightForFiles.assert_called_once_with(['some-file.java'], {'@address.com', 'Some Company'}, '@address.com')
+
+ @mock.patch('CopyrightCheck.CheckCopyrightForFiles')
+ @mock.patch('CopyrightCheck.FindAlteredFiles')
+ @mock.patch('CopyrightCheck.CheckCommitterInConfigFile')
+ @mock.patch('CopyrightCheck.ReadProjectCommittersConfigFile')
+ @mock.patch('CopyrightCheck.GetCommitterEmailExtension')
+ @mock.patch('CopyrightCheck.PermissionsCheck')
+ def test_MainNoFiles(self, mock_PermissionsCheck, mock_GetCommitterEmailExtension, mock_ReadProjectCommittersConfigFile,
+ mock_CheckCommitterInConfigFile, mock_FindAlteredFiles, mock_CheckCopyrightForFiles):
+
+ mock_GetCommitterEmailExtension.return_value = '@address.com'
+ mock_ReadProjectCommittersConfigFile.return_value = {'@address.com', 'Some Company'}
+ mock_FindAlteredFiles.return_value = []
+
+ capturedOutput = io.StringIO()
+ sys.stdout = capturedOutput # Capture output to stdout
+
+ CopyrightCheck.main()
+
+ sys.stdout = sys.__stdout__
+
+ expectedOutput = (BANNER + '\nCopyright Check Python Script:\n' +
+ '0 issue(s) found after 0 altered file(s) checked\n' +
+ BANNER + '\n')
+
+ self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+ mock_PermissionsCheck.assert_called_once_with()
+ mock_GetCommitterEmailExtension.assert_called_once_with()
+ mock_ReadProjectCommittersConfigFile.assert_called_once_with()
+ mock_CheckCommitterInConfigFile.assert_called_once_with('@address.com', {'@address.com', 'Some Company'})
+ mock_FindAlteredFiles.assert_called_once_with()
+ mock_CheckCopyrightForFiles.assert_not_called()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/cps-application/pom.xml b/cps-application/pom.xml
index 50b06b2e6..193599ff9 100755
--- a/cps-application/pom.xml
+++ b/cps-application/pom.xml
@@ -27,7 +27,7 @@
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
diff --git a/cps-bom/pom.xml b/cps-bom/pom.xml
index 3e5f70d77..e46892695 100644
--- a/cps-bom/pom.xml
+++ b/cps-bom/pom.xml
@@ -25,7 +25,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>cps-bom</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<description>This artifact contains dependencyManagement declarations of all published CPS components.</description>
@@ -37,6 +37,18 @@
<snapshotNexusPath>/content/repositories/snapshots/</snapshotNexusPath>
</properties>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <version>2.8.2</version>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+
<distributionManagement>
<repository>
<id>ecomp-releases</id>
diff --git a/cps-dependencies/pom.xml b/cps-dependencies/pom.xml
index f04213d43..dcbc5f70b 100755
--- a/cps-dependencies/pom.xml
+++ b/cps-dependencies/pom.xml
@@ -3,6 +3,7 @@
============LICENSE_START=======================================================
Copyright (c) 2021 Linux Foundation.
Modifications Copyright (C) 2020-2022 Nordix Foundation
+ Modifications Copyright (C) 2022 Bell Canada.
================================================================================
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,16 +16,18 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+
+ SPDX-License-Identifier: Apache-2.0
============LICENSE_END=========================================================
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>cps-dependencies</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>${project.groupId}:${project.artifactId}</name>
@@ -48,6 +51,18 @@
<mapstruct.version>1.4.2.Final</mapstruct.version>
</properties>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <version>2.8.2</version>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+
<distributionManagement>
<repository>
<id>ecomp-releases</id>
@@ -66,23 +81,18 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
- <version>2.5.5</version>
+ <version>2.6.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
- <version>2020.0.2</version>
+ <version>2021.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
- <groupId>org.springframework</groupId>
- <artifactId>spring-web</artifactId>
- <version>5.3.13</version>
- </dependency>
- <dependency>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>yangtools-artifacts</artifactId>
<version>6.0.1</version>
@@ -181,16 +191,6 @@
<scope>test</scope>
</dependency>
<dependency>
- <groupId>org.apache.logging.log4j</groupId>
- <artifactId>log4j-api</artifactId>
- <version>2.17.1</version>
- </dependency>
- <dependency>
- <groupId>org.apache.logging.log4j</groupId>
- <artifactId>log4j-to-slf4j</artifactId>
- <version>2.17.1</version>
- </dependency>
- <dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
diff --git a/cps-events/pom.xml b/cps-events/pom.xml
index b9b399c95..9bd958827 100644
--- a/cps-events/pom.xml
+++ b/cps-events/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
diff --git a/cps-ncmp-rest/docs/openapi/components.yaml b/cps-ncmp-rest/docs/openapi/components.yaml
index 69225aed2..7ed2efe52 100644
--- a/cps-ncmp-rest/docs/openapi/components.yaml
+++ b/cps-ncmp-rest/docs/openapi/components.yaml
@@ -1,6 +1,7 @@
# ============LICENSE_START=======================================================
# Copyright (C) 2021-2022 Nordix Foundation
# Modifications Copyright (C) 2021 Pantheon.tech
+# Modifications Copyright (C) 2022 Bell Canada
# ================================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -30,7 +31,23 @@ components:
type: string
details:
type: string
-
+ # DMI Server Exception Schema
+ DmiErrorMessage:
+ title: DMI Error Message
+ type: object
+ properties:
+ message:
+ type: string
+ example: "Bad Gateway Error Message NCMP"
+ dmi-response:
+ type: object
+ properties:
+ http-code:
+ type: integer
+ example: 400
+ body:
+ type: string
+ example: Bad Request
# Request Schemas
RestDmiPluginRegistration:
type: object
@@ -70,6 +87,33 @@ components:
items:
type: string
example: [my-cm-handle1, my-cm-handle2, my-cm-handle3]
+ DmiPluginRegistrationErrorResponse:
+ type: object
+ properties:
+ failedCreatedCmHandles:
+ type: array
+ items:
+ $ref: '#/components/schemas/CmHandlerRegistrationErrorResponse'
+ failedUpdatedCmHandles:
+ type: array
+ items:
+ $ref: '#/components/schemas/CmHandlerRegistrationErrorResponse'
+ failedRemovedCmHandles:
+ type: array
+ items:
+ $ref: '#/components/schemas/CmHandlerRegistrationErrorResponse'
+ CmHandlerRegistrationErrorResponse:
+ type: object
+ properties:
+ cmHandle:
+ type: string
+ example: my-cm-handle
+ errorCode:
+ type: string
+ example: '00'
+ errorText:
+ type: string
+ example: 'Unknown error. <error-details>'
RestInputCmHandle:
required:
@@ -146,6 +190,16 @@ components:
type: string
example: my-module-revision
+ CmHandleQueryRestParameters:
+ type: object
+ title: Cm Handle query parameters for executing cm handle search
+ properties:
+ publicCmHandleProperties:
+ type: object
+ additionalProperties:
+ type: string
+ example: Book Type
+
RestOutputCmHandle:
type: object
title: CM handle Details
@@ -303,14 +357,6 @@ components:
sample 3:
value:
resourceIdentifier: parent=shops,child=bookstore
- acceptParamInHeader:
- name: Accept
- in: header
- required: false
- description: Accept parameter for response, if accept parameter is null, that means client can accept any format.
- schema:
- type: string
- enum: [ application/json, application/yang-data+json ]
optionsParamInQuery:
name: options
in: query
@@ -434,3 +480,14 @@ components:
status: 500
message: Internal Server Error
details: Internal Server Error occurred
+ BadGateway:
+ description: Bad Gateway
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DmiErrorMessage"
+ example:
+ message: "Bad Gateway Error Message NCMP"
+ dmi-response:
+ http-code: 400
+ body: "Bad Request"
diff --git a/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml b/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml
index 3cd8e8baf..0a408c241 100755
--- a/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml
+++ b/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml
@@ -31,7 +31,7 @@ updateDmiRegistration:
schema:
$ref: 'components.yaml#/components/schemas/RestDmiPluginRegistration'
responses:
- 204:
+ 200:
$ref: 'components.yaml#/components/responses/NoContent'
400:
$ref: 'components.yaml#/components/responses/BadRequest'
@@ -40,4 +40,60 @@ updateDmiRegistration:
403:
$ref: 'components.yaml#/components/responses/Forbidden'
500:
- $ref: 'components.yaml#/components/responses/InternalServerError'
+ description: Partial or Complete failure. The error details are provided in the response body and all supported error codes are documented in the example.
+ content:
+ application/json:
+ schema:
+ $ref: 'components.yaml#/components/schemas/DmiPluginRegistrationErrorResponse'
+ example:
+ failedCreatedCmHandles: [
+ {
+ "cmHandle": "my-cm-handle-01",
+ "errorCode": "00",
+ "errorText": "Unknown error. <error-details>"
+ },
+ {
+ "cmHandle": "my-cm-handle-02",
+ "errorCode": "01",
+ "errorText": "cm-handle already exists"
+ },
+ {
+ "cmHandle": "my-cm-handle-03",
+ "errorCode": "03",
+ "errorText": "cm-handle has an invalid character(s) in id"
+ }
+ ]
+ failedUpdatedCmHandles: [
+ {
+ "cmHandle": "my-cm-handle-01",
+ "errorCode": "00",
+ "errorText": "Unknown error. <error-details>"
+ },
+ {
+ "cmHandle": "my-cm-handle-02",
+ "errorCode": "02",
+ "errorText": "cm-handle does not exist"
+ },
+ {
+ "cmHandle": "my-cm-handle-03",
+ "errorCode": "03",
+ "errorText": "cm-handle has an invalid character(s) in id"
+ }
+ ]
+ failedRemovedCmHandles: [
+ {
+ "cmHandle": "my-cm-handle-01",
+ "errorCode": "00",
+ "errorText": "Unknown error. <error-details>"
+ },
+ {
+ "cmHandle": "my-cm-handle-02",
+ "errorCode": "02",
+ "errorText": "cm-handle does not exists"
+ },
+ {
+ "cmHandle": "my-cm-handle-03",
+ "errorCode": "03",
+ "errorText": "cm-handle has an invalid character(s) in id"
+ }
+ ]
diff --git a/cps-ncmp-rest/docs/openapi/ncmp.yml b/cps-ncmp-rest/docs/openapi/ncmp.yml
index a9d08b795..05e4b8485 100755
--- a/cps-ncmp-rest/docs/openapi/ncmp.yml
+++ b/cps-ncmp-rest/docs/openapi/ncmp.yml
@@ -1,7 +1,7 @@
# ============LICENSE_START=======================================================
# Copyright (C) 2021-2022 Nordix Foundation
# Modifications Copyright (C) 2021 Pantheon.tech
-# Modifications Copyright (C) 2021 Bell Canada
+# Modifications Copyright (C) 2021-2022 Bell Canada
# ================================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -27,7 +27,6 @@ getResourceDataForPassthroughOperational:
parameters:
- $ref: 'components.yaml#/components/parameters/cmHandleInPath'
- $ref: 'components.yaml#/components/parameters/resourceIdentifierInQuery'
- - $ref: 'components.yaml#/components/parameters/acceptParamInHeader'
- $ref: 'components.yaml#/components/parameters/optionsParamInQuery'
- $ref: 'components.yaml#/components/parameters/topicParamInQuery'
responses:
@@ -48,6 +47,8 @@ getResourceDataForPassthroughOperational:
$ref: 'components.yaml#/components/responses/Forbidden'
500:
$ref: 'components.yaml#/components/responses/InternalServerError'
+ 502:
+ $ref: 'components.yaml#/components/responses/BadGateway'
resourceDataForPassthroughRunning:
get:
@@ -59,7 +60,6 @@ resourceDataForPassthroughRunning:
parameters:
- $ref: 'components.yaml#/components/parameters/cmHandleInPath'
- $ref: 'components.yaml#/components/parameters/resourceIdentifierInQuery'
- - $ref: 'components.yaml#/components/parameters/acceptParamInHeader'
- $ref: 'components.yaml#/components/parameters/optionsParamInQuery'
- $ref: 'components.yaml#/components/parameters/topicParamInQuery'
responses:
@@ -80,6 +80,8 @@ resourceDataForPassthroughRunning:
$ref: 'components.yaml#/components/responses/Forbidden'
500:
$ref: 'components.yaml#/components/responses/InternalServerError'
+ 502:
+ $ref: 'components.yaml#/components/responses/BadGateway'
post:
tags:
- network-cm-proxy
@@ -116,6 +118,8 @@ resourceDataForPassthroughRunning:
$ref: 'components.yaml#/components/responses/Forbidden'
500:
$ref: 'components.yaml#/components/responses/InternalServerError'
+ 502:
+ $ref: 'components.yaml#/components/responses/BadGateway'
put:
tags:
@@ -153,6 +157,8 @@ resourceDataForPassthroughRunning:
$ref: 'components.yaml#/components/responses/Forbidden'
500:
$ref: 'components.yaml#/components/responses/InternalServerError'
+ 502:
+ $ref: 'components.yaml#/components/responses/BadGateway'
patch:
tags:
@@ -184,6 +190,8 @@ resourceDataForPassthroughRunning:
$ref: 'components.yaml#/components/responses/Forbidden'
500:
$ref: 'components.yaml#/components/responses/InternalServerError'
+ 502:
+ $ref: 'components.yaml#/components/responses/BadGateway'
delete:
tags:
@@ -208,6 +216,8 @@ resourceDataForPassthroughRunning:
$ref: 'components.yaml#/components/responses/NotFound'
500:
$ref: 'components.yaml#/components/responses/InternalServerError'
+ 502:
+ $ref: 'components.yaml#/components/responses/BadGateway'
fetchModuleReferencesByCmHandle:
get:
@@ -281,6 +291,33 @@ retrieveCmHandleDetailsById:
application/json:
schema:
$ref: 'components.yaml#/components/schemas/RestOutputCmHandle'
+ 404:
+ $ref: 'components.yaml#/components/responses/NotFound'
+ 500:
+ $ref: 'components.yaml#/components/responses/InternalServerError'
+
+queryCmHandles:
+ post:
+ description: Execute cm handle query search
+ tags:
+ - network-cm-proxy
+ summary: Execute cm handle query upon a given set of query parameters
+ operationId: queryCmHandles
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: 'components.yaml#/components/schemas/CmHandleQueryRestParameters'
+ responses:
+ 200:
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: string
400:
$ref: 'components.yaml#/components/responses/BadRequest'
401:
diff --git a/cps-ncmp-rest/docs/openapi/openapi.yml b/cps-ncmp-rest/docs/openapi/openapi.yml
index 12a8318ef..935b657e1 100755
--- a/cps-ncmp-rest/docs/openapi/openapi.yml
+++ b/cps-ncmp-rest/docs/openapi/openapi.yml
@@ -39,4 +39,7 @@ paths:
$ref: 'ncmp.yml#/executeCmHandleSearch'
/v1/ch/{cm-handle}:
- $ref: 'ncmp.yml#/retrieveCmHandleDetailsById' \ No newline at end of file
+ $ref: 'ncmp.yml#/retrieveCmHandleDetailsById'
+
+ /v1/data/ch/searches:
+ $ref: 'ncmp.yml#/queryCmHandles'
diff --git a/cps-ncmp-rest/pom.xml b/cps-ncmp-rest/pom.xml
index 97305cfe9..6a700c3e1 100644
--- a/cps-ncmp-rest/pom.xml
+++ b/cps-ncmp-rest/pom.xml
@@ -27,7 +27,7 @@
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapper.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapper.java
index 4c8fafea5..a9ec863d5 100644
--- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapper.java
+++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapper.java
@@ -45,7 +45,7 @@ public interface NcmpRestInputMapper {
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT)
DmiPluginRegistration toDmiPluginRegistration(final RestDmiPluginRegistration restDmiPluginRegistration);
- @Mapping(source = "cmHandle", target = "cmHandleID")
+ @Mapping(source = "cmHandle", target = "cmHandleId")
@Mapping(source = "cmHandleProperties", target = "dmiProperties")
@Mapping(source = "publicCmHandleProperties", target = "publicProperties")
NcmpServiceCmHandle toNcmpServiceCmHandle(final RestInputCmHandle restInputCmHandle);
diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java
index 0201fad2b..5c1f8704d 100755
--- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java
+++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java
@@ -3,7 +3,7 @@
* Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2021-2022 Nordix Foundation
* Modification Copyright (C) 2021 highstreet technologies GmbH
- * Modifications (C) 2021 Bell Canada
+ * Modifications (C) 2021-2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,18 +31,25 @@ import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
+import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException;
+import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters;
import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
import org.onap.cps.ncmp.rest.api.NetworkCmProxyApi;
import org.onap.cps.ncmp.rest.model.CmHandleProperties;
import org.onap.cps.ncmp.rest.model.CmHandleProperty;
import org.onap.cps.ncmp.rest.model.CmHandlePublicProperties;
+import org.onap.cps.ncmp.rest.model.CmHandleQueryRestParameters;
import org.onap.cps.ncmp.rest.model.CmHandles;
import org.onap.cps.ncmp.rest.model.ConditionProperties;
import org.onap.cps.ncmp.rest.model.Conditions;
@@ -50,6 +57,7 @@ import org.onap.cps.ncmp.rest.model.ModuleNameAsJsonObject;
import org.onap.cps.ncmp.rest.model.ModuleNamesAsJsonArray;
import org.onap.cps.ncmp.rest.model.RestModuleReference;
import org.onap.cps.ncmp.rest.model.RestOutputCmHandle;
+import org.onap.cps.utils.CpsValidator;
import org.onap.cps.utils.JsonObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -63,6 +71,9 @@ import org.springframework.web.bind.annotation.RestController;
public class NetworkCmProxyController implements NetworkCmProxyApi {
private static final String NO_BODY = null;
+ private static final String NO_REQUEST_ID = null;
+ private static final String NO_TOPIC = null;
+ public static final String ASYNC_REQUEST_ID = "requestId";
private final NetworkCmProxyDataService networkCmProxyDataService;
private final JsonObjectMapper jsonObjectMapper;
@@ -73,7 +84,6 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
*
* @param cmHandle cm handle identifier
* @param resourceIdentifier resource identifier
- * @param acceptParamInHeader accept header parameter
* @param optionsParamInQuery options query parameter
* @param topicParamInQuery topic query parameter
* @return {@code ResponseEntity} response from dmi plugin
@@ -81,15 +91,21 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
@Override
public ResponseEntity<Object> getResourceDataOperationalForCmHandle(final String cmHandle,
final @NotNull @Valid String resourceIdentifier,
- final String acceptParamInHeader,
final @Valid String optionsParamInQuery,
final @Valid String topicParamInQuery) {
+ final ResponseEntity<Map<String, Object>> asyncResponse = populateAsyncResponse(topicParamInQuery);
+ final Map<String, Object> asyncResponseData = asyncResponse.getBody();
+
final Object responseObject = networkCmProxyDataService.getResourceDataOperationalForCmHandle(cmHandle,
resourceIdentifier,
- acceptParamInHeader,
optionsParamInQuery,
- topicParamInQuery);
- return ResponseEntity.ok(responseObject);
+ asyncResponseData == null ? NO_TOPIC : topicParamInQuery,
+ asyncResponseData == null ? NO_REQUEST_ID : asyncResponseData.get(ASYNC_REQUEST_ID).toString());
+
+ if (asyncResponseData == null) {
+ return ResponseEntity.ok(responseObject);
+ }
+ return ResponseEntity.ok(asyncResponse);
}
/**
@@ -97,7 +113,6 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
*
* @param cmHandle cm handle identifier
* @param resourceIdentifier resource identifier
- * @param acceptParamInHeader accept header parameter
* @param optionsParamInQuery options query parameter
* @param topicParamInQuery topic query parameter
* @return {@code ResponseEntity} response from dmi plugin
@@ -105,15 +120,21 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
@Override
public ResponseEntity<Object> getResourceDataRunningForCmHandle(final String cmHandle,
final @NotNull @Valid String resourceIdentifier,
- final String acceptParamInHeader,
final @Valid String optionsParamInQuery,
final @Valid String topicParamInQuery) {
+ final ResponseEntity<Map<String, Object>> asyncResponse = populateAsyncResponse(topicParamInQuery);
+ final Map<String, Object> asyncResponseData = asyncResponse.getBody();
+
final Object responseObject = networkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle(cmHandle,
resourceIdentifier,
- acceptParamInHeader,
optionsParamInQuery,
- topicParamInQuery);
- return ResponseEntity.ok(responseObject);
+ asyncResponseData == null ? NO_TOPIC : topicParamInQuery,
+ asyncResponseData == null ? NO_REQUEST_ID : asyncResponseData.get(ASYNC_REQUEST_ID).toString());
+
+ if (asyncResponseData == null) {
+ return ResponseEntity.ok(responseObject);
+ }
+ return ResponseEntity.ok(asyncResponse);
}
@Override
@@ -195,6 +216,19 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
}
/**
+ * Query and return cm handles that match the given query parameters.
+ *
+ * @param cmHandleQueryRestParameters the cm handle query parameters
+ * @return collection of cm handle ids
+ */
+ public ResponseEntity<List<String>> queryCmHandles(
+ final CmHandleQueryRestParameters cmHandleQueryRestParameters) {
+ final Set<String> cmHandleIds = networkCmProxyDataService.queryCmHandles(
+ jsonObjectMapper.convertToValueType(cmHandleQueryRestParameters, CmHandleQueryApiParameters.class));
+ return ResponseEntity.ok(List.copyOf(cmHandleIds));
+ }
+
+ /**
* Search for Cm Handle and Properties by Name.
* @param cmHandleId cm-handle identifier
* @return cm handle and its properties
@@ -258,9 +292,38 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
private RestOutputCmHandle toRestOutputCmHandle(final NcmpServiceCmHandle ncmpServiceCmHandle) {
final RestOutputCmHandle restOutputCmHandle = new RestOutputCmHandle();
final CmHandlePublicProperties cmHandlePublicProperties = new CmHandlePublicProperties();
- restOutputCmHandle.setCmHandle(ncmpServiceCmHandle.getCmHandleID());
+ restOutputCmHandle.setCmHandle(ncmpServiceCmHandle.getCmHandleId());
cmHandlePublicProperties.add(ncmpServiceCmHandle.getPublicProperties());
restOutputCmHandle.setPublicCmHandleProperties(cmHandlePublicProperties);
return restOutputCmHandle;
}
+
+ private ResponseEntity<Map<String, Object>> populateAsyncResponse(final String topicParamInQuery) {
+ final boolean processAsynchronously = hasTopicParameter(topicParamInQuery);
+ final Map<String, Object> responseData;
+ if (processAsynchronously) {
+ responseData = getAsyncResponseData();
+ } else {
+ responseData = null;
+ }
+ return ResponseEntity.ok().body(responseData);
+ }
+
+ private static boolean hasTopicParameter(final String topicName) {
+ if (topicName == null) {
+ return false;
+ }
+ if (CpsValidator.validateTopicName(topicName)) {
+ return true;
+ }
+ throw new InvalidTopicException("Topic name " + topicName + " is invalid", "invalid topic");
+ }
+
+ private Map<String, Object> getAsyncResponseData() {
+ final Map<String, Object> asyncResponseData = new HashMap<>(1);
+ final String resourceDataRequestId = UUID.randomUUID().toString();
+ asyncResponseData.put(ASYNC_REQUEST_ID, resourceDataRequestId);
+ return asyncResponseData;
+ }
+
}
diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java
index c9d26f2a5..105a6a559 100755
--- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java
+++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021 Bell Canada
+ * Copyright (C) 2021-2022 Bell Canada
* Modifications Copyright (C) 2022 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,10 +21,17 @@
package org.onap.cps.ncmp.rest.controller;
+import java.util.List;
+import java.util.stream.Collectors;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status;
+import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse;
import org.onap.cps.ncmp.rest.api.NetworkCmProxyInventoryApi;
+import org.onap.cps.ncmp.rest.model.CmHandlerRegistrationErrorResponse;
+import org.onap.cps.ncmp.rest.model.DmiPluginRegistrationErrorResponse;
import org.onap.cps.ncmp.rest.model.RestDmiPluginRegistration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -41,14 +48,58 @@ public class NetworkCmProxyInventoryController implements NetworkCmProxyInventor
/**
* Update DMI Plugin Registration (used for first registration also).
+ *
* @param restDmiPluginRegistration the registration data
*/
@Override
- public ResponseEntity<Void> updateDmiPluginRegistration(
+ public ResponseEntity updateDmiPluginRegistration(
final @Valid RestDmiPluginRegistration restDmiPluginRegistration) {
- networkCmProxyDataService.updateDmiRegistrationAndSyncModule(
- ncmpRestInputMapper.toDmiPluginRegistration(restDmiPluginRegistration));
- return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ final DmiPluginRegistrationResponse dmiPluginRegistrationResponse =
+ networkCmProxyDataService.updateDmiRegistrationAndSyncModule(
+ ncmpRestInputMapper.toDmiPluginRegistration(restDmiPluginRegistration));
+ final DmiPluginRegistrationErrorResponse failedRegistrationErrorResponse =
+ getFailureRegistrationResponse(dmiPluginRegistrationResponse);
+ return allRegistrationsSuccessful(failedRegistrationErrorResponse)
+ ? new ResponseEntity<>(HttpStatus.OK)
+ : new ResponseEntity<>(failedRegistrationErrorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ private boolean allRegistrationsSuccessful(
+ final DmiPluginRegistrationErrorResponse dmiPluginRegistrationErrorResponse) {
+ return dmiPluginRegistrationErrorResponse.getFailedCreatedCmHandles().isEmpty()
+ && dmiPluginRegistrationErrorResponse.getFailedUpdatedCmHandles().isEmpty()
+ && dmiPluginRegistrationErrorResponse.getFailedRemovedCmHandles().isEmpty();
+
+ }
+
+ private DmiPluginRegistrationErrorResponse getFailureRegistrationResponse(
+ final DmiPluginRegistrationResponse dmiPluginRegistrationResponse) {
+ final DmiPluginRegistrationErrorResponse dmiPluginRegistrationErrorResponse =
+ new DmiPluginRegistrationErrorResponse();
+ dmiPluginRegistrationErrorResponse.setFailedCreatedCmHandles(
+ getFailedResponses(dmiPluginRegistrationResponse.getCreatedCmHandles()));
+ dmiPluginRegistrationErrorResponse.setFailedUpdatedCmHandles(
+ getFailedResponses(dmiPluginRegistrationResponse.getUpdatedCmHandles()));
+ dmiPluginRegistrationErrorResponse.setFailedRemovedCmHandles(
+ getFailedResponses(dmiPluginRegistrationResponse.getRemovedCmHandles()));
+
+ return dmiPluginRegistrationErrorResponse;
+ }
+
+ private List<CmHandlerRegistrationErrorResponse> getFailedResponses(
+ final List<CmHandleRegistrationResponse> cmHandleRegistrationResponseList) {
+ return cmHandleRegistrationResponseList.stream()
+ .filter(cmHandleRegistrationResponse -> cmHandleRegistrationResponse.getStatus() == Status.FAILURE)
+ .map(this::toCmHandleRegistrationErrorResponse)
+ .collect(Collectors.toList());
+ }
+
+ private CmHandlerRegistrationErrorResponse toCmHandleRegistrationErrorResponse(
+ final CmHandleRegistrationResponse registrationResponse) {
+ return new CmHandlerRegistrationErrorResponse()
+ .cmHandle(registrationResponse.getCmHandle())
+ .errorCode(registrationResponse.getRegistrationError().errorCode)
+ .errorText(registrationResponse.getErrorText());
}
}
diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java
index 0843e9741..c72373344 100755
--- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java
+++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java
@@ -24,11 +24,14 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.ncmp.api.impl.exception.DmiRequestException;
+import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException;
import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException;
import org.onap.cps.ncmp.api.impl.exception.NcmpException;
import org.onap.cps.ncmp.api.impl.exception.ServerNcmpException;
import org.onap.cps.ncmp.rest.controller.NetworkCmProxyController;
import org.onap.cps.ncmp.rest.controller.NetworkCmProxyInventoryController;
+import org.onap.cps.ncmp.rest.model.DmiErrorMessage;
+import org.onap.cps.ncmp.rest.model.DmiErrorMessageDmiresponse;
import org.onap.cps.ncmp.rest.model.ErrorMessage;
import org.onap.cps.spi.exceptions.CpsException;
import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
@@ -66,6 +69,12 @@ public class NetworkCmProxyRestExceptionHandler {
return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, exception);
}
+ @ExceptionHandler({HttpClientRequestException.class})
+ public static ResponseEntity<Object> handleClientRequestExceptions(
+ final HttpClientRequestException httpClientRequestException) {
+ return wrapDmiErrorResponse(HttpStatus.BAD_GATEWAY, httpClientRequestException);
+ }
+
@ExceptionHandler({DmiRequestException.class, DataValidationException.class, HttpMessageNotReadableException.class,
InvalidTopicException.class})
public static ResponseEntity<Object> handleDmiRequestExceptions(final Exception exception) {
@@ -91,8 +100,19 @@ public class NetworkCmProxyRestExceptionHandler {
} else {
errorMessage.setDetails(CHECK_LOGS_FOR_DETAILS);
}
- errorMessage.setDetails(exception instanceof CpsException ? ((CpsException) exception).getDetails() :
- CHECK_LOGS_FOR_DETAILS);
+ errorMessage.setDetails(
+ exception instanceof CpsException ? ((CpsException) exception).getDetails() : CHECK_LOGS_FOR_DETAILS);
return new ResponseEntity<>(errorMessage, status);
}
+
+ private static ResponseEntity<Object> wrapDmiErrorResponse(final HttpStatus httpStatus,
+ final HttpClientRequestException httpClientRequestException) {
+ final var dmiErrorMessage = new DmiErrorMessage();
+ final var dmiErrorResponse = new DmiErrorMessageDmiresponse();
+ dmiErrorResponse.setHttpCode(httpClientRequestException.getHttpStatus());
+ dmiErrorResponse.setBody(httpClientRequestException.getDetails());
+ dmiErrorMessage.setMessage(httpClientRequestException.getMessage());
+ dmiErrorMessage.setDmiResponse(dmiErrorResponse);
+ return new ResponseEntity<>(dmiErrorMessage, httpStatus);
+ }
}
diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapperSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapperSpec.groovy
index 3d54a0b08..bb762080d 100644
--- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapperSpec.groovy
+++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapperSpec.groovy
@@ -43,7 +43,7 @@ class NcmpRestInputMapperSpec extends Specification {
then: 'the result returns the correct number of cm handles'
result.createdCmHandles.size() == 1
and: 'the converted cm handle has the same id'
- result.createdCmHandles[0].cmHandleID == 'example-id'
+ result.createdCmHandles[0].cmHandleId == 'example-id'
and: '(empty) properties are converted correctly'
result.createdCmHandles[0].dmiProperties == expectedDmiProperties
result.createdCmHandles[0].publicProperties == expectedPublicProperties
diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy
index d5c3cd9f3..b34b0fff3 100644
--- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy
+++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy
@@ -3,7 +3,7 @@
* Copyright (C) 2021 Pantheon.tech
* Modification Copyright (C) 2021 highstreet technologies GmbH
* Modification Copyright (C) 2021-2022 Nordix Foundation
- * Modification Copyright (C) 2021 Bell Canada.
+ * Modification Copyright (C) 2021-2022 Bell Canada.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -60,7 +60,10 @@ class NetworkCmProxyControllerSpec extends Specification {
NetworkCmProxyDataService mockNetworkCmProxyDataService = Mock()
@SpringBean
- JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+ ObjectMapper objectMapper = new ObjectMapper()
+
+ @SpringBean
+ JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper)
@SpringBean
NcmpRestInputMapper ncmpRestInputMapper = Mappers.getMapper(NcmpRestInputMapper)
@@ -72,6 +75,7 @@ class NetworkCmProxyControllerSpec extends Specification {
@Shared
def NO_TOPIC = null
+ def NO_REQUEST_ID = null
def 'Get Resource Data from pass-through operational.'() {
given: 'resource data url'
@@ -81,43 +85,51 @@ class NetworkCmProxyControllerSpec extends Specification {
def response = mvc.perform(
get(getUrl)
.contentType(MediaType.APPLICATION_JSON)
- .accept(MediaType.APPLICATION_JSON_VALUE)
).andReturn().response
then: 'the NCMP data service is called with getResourceDataOperationalForCmHandle'
1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle',
'parent/child',
- 'application/json',
'(a=1,b=2)',
- NO_TOPIC)
+ NO_TOPIC,
+ NO_REQUEST_ID)
and: 'response status is Ok'
response.status == HttpStatus.OK.value()
}
- def 'Get Resource Data from pass-through operational with #scenario.'() {
+ def 'Get Resource Data from #datastoreInUrl with #scenario.'() {
given: 'resource data url'
- def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" +
+ def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
"?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}"
when: 'get data resource request is performed'
def response = mvc.perform(
get(getUrl)
.contentType(MediaType.APPLICATION_JSON)
- .accept(MediaType.APPLICATION_JSON_VALUE)
).andReturn().response
then: 'the NCMP data service is called with operational data for cm handle'
- 1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle',
+ expectedNumberOfMethodExecutions
+ * mockNetworkCmProxyDataService."${expectedMethodName}"('testCmHandle',
'parent/child',
- 'application/json',
'(a=1,b=2)',
- expectedTopicName)
- and: 'response status is Ok'
- response.status == HttpStatus.OK.value()
+ expectedTopicName,
+ _)
+ then: 'response status is expected'
+ response.status == expectedHttpStatus
where: 'the following parameters are used'
- scenario | topicQueryParam || expectedTopicName
- 'Url with valid topic' | "&topic=my-topic-name" || "my-topic-name"
- 'No topic in url' | '' || NO_TOPIC
- 'Null topic in url' | "&topic=null" || "null"
- 'Empty topic in url' | "&topic=\"\"" || "\"\""
- 'Missing topic in url' | "&topic=" || ""
+ scenario | datastoreInUrl | topicQueryParam || expectedTopicName | expectedMethodName | expectedNumberOfMethodExecutions | expectedHttpStatus
+ 'url with valid topic' | 'passthrough-operational' | '&topic=my-topic-name' || 'my-topic-name' | 'getResourceDataOperationalForCmHandle' | 1 | HttpStatus.OK.value()
+ 'no topic in url' | 'passthrough-operational' | '' || NO_TOPIC | 'getResourceDataOperationalForCmHandle' | 1 | HttpStatus.OK.value()
+ 'null topic in url' | 'passthrough-operational' | '&topic=null' || 'null' | 'getResourceDataOperationalForCmHandle' | 1 | HttpStatus.OK.value()
+ 'empty topic in url' | 'passthrough-operational' | '&topic=\"\"' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value()
+ 'missing topic in url' | 'passthrough-operational' | '&topic=' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value()
+ 'blank topic value in url' | 'passthrough-operational' | '&topic=\" \"' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value()
+ 'invalid non-empty topic value in url' | 'passthrough-operational' | '&topic=1_5_*_#' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value()
+ 'url with valid topic' | 'passthrough-running' | '&topic=my-topic-name' || 'my-topic-name' | 'getResourceDataPassThroughRunningForCmHandle' | 1 | HttpStatus.OK.value()
+ 'no topic in url' | 'passthrough-running' | '' || NO_TOPIC | 'getResourceDataPassThroughRunningForCmHandle' | 1 | HttpStatus.OK.value()
+ 'null topic in url' | 'passthrough-running' | '&topic=null' || 'null' | 'getResourceDataPassThroughRunningForCmHandle' | 1 | HttpStatus.OK.value()
+ 'empty topic in url' | 'passthrough-running' | '&topic=\"\"' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value()
+ 'missing topic in url' | 'passthrough-running' | '&topic=' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value()
+ 'blank topic value in url' | 'passthrough-running' | '&topic=\" \"' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value()
+ 'invalid non-empty topic value in url' | 'passthrough-running' | '&topic=1_5_*_#' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value()
}
def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() {
@@ -127,14 +139,13 @@ class NetworkCmProxyControllerSpec extends Specification {
and: 'ncmp service returns json object'
mockNetworkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
resourceIdentifier,
- 'application/json',
'(a=1,b=2)',
- NO_TOPIC) >> '{valid-json}'
+ NO_TOPIC,
+ NO_REQUEST_ID) >> '{valid-json}'
when: 'get data resource request is performed'
def response = mvc.perform(
get(getUrl)
.contentType(MediaType.APPLICATION_JSON)
- .accept(MediaType.APPLICATION_JSON_VALUE)
).andReturn().response
then: 'response status is Ok'
response.status == HttpStatus.OK.value()
@@ -157,8 +168,7 @@ class NetworkCmProxyControllerSpec extends Specification {
when: 'update data resource request is performed'
def response = mvc.perform(
put(updateUrl)
- .contentType(MediaType.APPLICATION_JSON_VALUE)
- .accept(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
+ .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
).andReturn().response
then: 'ncmp service method to update resource is called'
1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
@@ -175,8 +185,7 @@ class NetworkCmProxyControllerSpec extends Specification {
when: 'create resource request is performed'
def response = mvc.perform(
post(url)
- .contentType(MediaType.APPLICATION_JSON_VALUE)
- .accept(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
+ .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
).andReturn().response
then: 'ncmp service method to create resource called'
1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
@@ -222,7 +231,7 @@ class NetworkCmProxyControllerSpec extends Specification {
def cmHandleId = 'Some-Cm-Handle'
def dmiProperties = [ prop:'some DMI property' ]
def publicProperties = [ "public prop":'some public property' ]
- def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleID: cmHandleId, dmiProperties: dmiProperties, publicProperties: publicProperties)
+ def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: dmiProperties, publicProperties: publicProperties)
and: 'the service method is invoked with the cm handle id'
1 * mockNetworkCmProxyDataService.getNcmpServiceCmHandle('Some-Cm-Handle') >> ncmpServiceCmHandle
when: 'the cm handle details api is invoked'
@@ -249,6 +258,31 @@ class NetworkCmProxyControllerSpec extends Specification {
response.contentAsString == '{"cmHandles":[]}'
}
+ def 'Query for cm handles matching query parameters'() {
+ given: 'an endpoint and json data'
+ def searchesEndpoint = "$ncmpBasePathV1/data/ch/searches"
+ String jsonString = '{"publicCmHandleProperties": {"name": "Contact", "value": "newemailforstore@bookstore.com"}}'
+ and: 'the service method is invoked with module names and returns cm handle ids'
+ 1 * mockNetworkCmProxyDataService.queryCmHandles(_) >> ['some-cmhandle-id1', 'some-cmhandle-id2']
+ when: 'the searches api is invoked'
+ def response = mvc.perform(post(searchesEndpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonString)).andReturn().response
+ then: 'cm handle ids are returned'
+ response.contentAsString == '["some-cmhandle-id1","some-cmhandle-id2"]'
+ }
+
+ def 'Query for cm handles with invalid request payload'() {
+ when: 'the searches api is invoked'
+ def searchesEndpoint = "$ncmpBasePathV1/data/ch/searches"
+ def invalidInputData = '{invalidJson}'
+ def response = mvc.perform(post(searchesEndpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(invalidInputData)).andReturn().response
+ then: 'BAD_REQUEST is returned'
+ response.getStatus() == 400
+ }
+
def 'Patch resource data in pass-through running datastore.' () {
given: 'patch resource data url'
def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
@@ -279,5 +313,24 @@ class NetworkCmProxyControllerSpec extends Specification {
and: 'the response is No Content'
response.status == HttpStatus.NO_CONTENT.value()
}
+
+ def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() {
+ given: 'resource data url'
+ def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
+ "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name"
+ when: 'get data resource request is performed'
+ def response = mvc.perform(
+ get(getUrl)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ ).andReturn().response
+ then: 'async request id is generated'
+ assert response.contentAsString.contains("requestId")
+ where: 'the following parameters are used'
+ scenario | datastoreInUrl
+ ':passthrough-operational' | 'passthrough-operational'
+ ':passthrough-running' | 'passthrough-running'
+ }
+
}
diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy
index 9b1c2e87c..30b6beb37 100644
--- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy
+++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021 Bell Canada
+ * Copyright (C) 2021-2022 Bell Canada
* Modifications Copyright (C) 2021-2022 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,7 +24,11 @@ package org.onap.cps.ncmp.rest.controller
import com.fasterxml.jackson.databind.ObjectMapper
import org.onap.cps.TestUtils
import org.onap.cps.ncmp.api.NetworkCmProxyDataService
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse
import org.onap.cps.ncmp.api.models.DmiPluginRegistration
+import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse
+import org.onap.cps.ncmp.rest.model.CmHandlerRegistrationErrorResponse
+import org.onap.cps.ncmp.rest.model.DmiPluginRegistrationErrorResponse
import org.onap.cps.ncmp.rest.model.RestDmiPluginRegistration
import org.onap.cps.utils.JsonObjectMapper
import org.spockframework.spring.SpringBean
@@ -36,6 +40,9 @@ import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import spock.lang.Specification
+
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_ALREADY_EXIST
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_DOES_NOT_EXIST
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
@WebMvcTest(NetworkCmProxyInventoryController)
@@ -58,7 +65,7 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
@Value('${rest.api.ncmp-inventory-base-path}/v1')
def ncmpBasePathV1
- def 'Dmi plugin registration #scenario' () {
+ def 'Dmi plugin registration #scenario'() {
given: 'a dmi plugin registration with #scenario'
def jsonData = TestUtils.getResourceFileContent(dmiRegistrationJson)
and: 'the expected rest input as an object'
@@ -72,9 +79,9 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
.content(jsonData)
).andReturn().response
then: 'the converted object is forwarded to the registration service'
- 1 * mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(mockDmiPluginRegistration)
+ 1 * mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(mockDmiPluginRegistration) >> new DmiPluginRegistrationResponse()
and: 'response status is no content'
- response.status == HttpStatus.NO_CONTENT.value()
+ response.status == HttpStatus.OK.value()
where: 'the following registration json is used'
scenario | dmiRegistrationJson
'multiple services, added, updated and removed cm handles and many properties' | 'dmi_registration_all_singing_and_dancing.json'
@@ -82,7 +89,7 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
'without any properties' | 'dmi_registration_without_properties.json'
}
- def 'Dmi plugin registration with invalid json' () {
+ def 'Dmi plugin registration with invalid json'() {
given: 'a dmi plugin registration with #scenario'
def jsonDataWithUndefinedDataLabel = '{"notAdmiPlugin":""}'
when: 'post request is performed & registration is called with correct DMI plugin information'
@@ -95,4 +102,74 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
response.status == HttpStatus.BAD_REQUEST.value()
}
+ def 'DMI Registration: All cm-handles operations processed successfully.'() {
+ given: 'a dmi plugin registration'
+ def dmiRegistrationRequest = '{}'
+ and: 'service can register cm-handles successfully'
+ def dmiRegistrationResponse = new DmiPluginRegistrationResponse(
+ createdCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-1')],
+ updatedCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-2')],
+ removedCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-3')]
+ )
+ mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(*_) >> dmiRegistrationResponse
+ when: 'registration endpoint is invoked'
+ def response = mvc.perform(
+ post("$ncmpBasePathV1/ch")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(dmiRegistrationRequest)
+ ).andReturn().response
+ then: 'response status is ok'
+ response.status == HttpStatus.OK.value()
+ and: 'the response body is empty'
+ response.getContentAsString() == ''
+
+ }
+
+ def 'DMI Registration Error Handling: #scenario.'() {
+ given: 'a dmi plugin registration'
+ def dmiRegistrationRequest = '{}'
+ and: '#scenario: service failed to register few cm-handle'
+ def dmiRegistrationResponse = new DmiPluginRegistrationResponse(
+ createdCmHandles: [createCmHandleResponse],
+ updatedCmHandles: [updateCmHandleResponse],
+ removedCmHandles: [removeCmHandleResponse]
+ )
+ mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(*_) >> dmiRegistrationResponse
+ when: 'registration endpoint is invoked'
+ def response = mvc.perform(
+ post("$ncmpBasePathV1/ch")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(dmiRegistrationRequest)
+ ).andReturn().response
+ then: 'request status is internal server error'
+ response.status == HttpStatus.INTERNAL_SERVER_ERROR.value()
+ and: 'the response body is in the expected format'
+ def responseBody = jsonObjectMapper.convertJsonString(response.getContentAsString(), DmiPluginRegistrationErrorResponse)
+ and: 'contains only the failure responses'
+ responseBody.getFailedCreatedCmHandles() == expectedFailedCreatedCmHandle
+ responseBody.getFailedUpdatedCmHandles() == expectedFailedUpdateCmHandle
+ responseBody.getFailedRemovedCmHandles() == expectedFailedRemovedCmHandle
+ where:
+ scenario | createCmHandleResponse | updateCmHandleResponse | removeCmHandleResponse || expectedFailedCreatedCmHandle | expectedFailedUpdateCmHandle | expectedFailedRemovedCmHandle
+ 'only create failed' | failedResponse('cm-handle-1') | successResponse('cm-handle-2') | successResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | [] | []
+ 'only update failed' | successResponse('cm-handle-1') | failedResponse('cm-handle-2') | successResponse('cm-handle-3') || [] | [failedRestResponse('cm-handle-2')] | []
+ 'only delete failed' | successResponse('cm-handle-1') | successResponse('cm-handle-2') | failedResponse('cm-handle-3') || [] | [] | [failedRestResponse('cm-handle-3')]
+ 'all three failed' | failedResponse('cm-handle-1') | failedResponse('cm-handle-2') | failedResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | [failedRestResponse('cm-handle-2')] | [failedRestResponse('cm-handle-3')]
+ 'create update failed' | failedResponse('cm-handle-1') | failedResponse('cm-handle-2') | successResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | [failedRestResponse('cm-handle-2')] | []
+ 'create delete failed' | failedResponse('cm-handle-1') | successResponse('cm-handle-2') | failedResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | [] | [failedRestResponse('cm-handle-3')]
+ 'update delete failed' | successResponse('cm-handle-1') | failedResponse('cm-handle-2') | failedResponse('cm-handle-3') || [] | [failedRestResponse('cm-handle-2')] | [failedRestResponse('cm-handle-3')]
+ }
+
+ def failedRestResponse(cmHandle) {
+ return new CmHandlerRegistrationErrorResponse('cmHandle': cmHandle, 'errorCode': '00', 'errorText': 'Failed')
+ }
+
+ def failedResponse(cmHandle) {
+ return CmHandleRegistrationResponse.createFailureResponse(cmHandle, new RuntimeException("Failed"))
+ }
+
+ def successResponse(cmHandle) {
+ return CmHandleRegistrationResponse.createSuccessResponse(cmHandle)
+ }
+
}
diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy
index b64237015..1f6c38428 100644
--- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy
+++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy
@@ -21,14 +21,13 @@
package org.onap.cps.ncmp.rest.exceptions
-import com.fasterxml.jackson.databind.ObjectMapper
import groovy.json.JsonSlurper
import org.mapstruct.factory.Mappers
import org.onap.cps.TestUtils
import org.onap.cps.ncmp.api.NetworkCmProxyDataService
import org.onap.cps.ncmp.api.impl.exception.DmiRequestException
+import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException
import org.onap.cps.ncmp.api.impl.exception.ServerNcmpException
-import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
import org.onap.cps.ncmp.rest.controller.NcmpRestInputMapper
import org.onap.cps.spi.exceptions.CpsException
import org.onap.cps.spi.exceptions.DataNodeNotFoundException
@@ -38,6 +37,7 @@ import org.spockframework.spring.SpringBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import spock.lang.Shared
@@ -111,6 +111,19 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification {
assertTestResponse(response, BAD_REQUEST, sampleErrorMessage, sampleErrorDetails)
}
+ def 'Failing DMI Request - passthrough scenario'() {
+ given: 'failing DMI request'
+ mockNetworkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle(*_) >> { throw new HttpClientRequestException('Error Message Details NCMP', 'Bad Request from DMI', 400) }
+ when: 'the DMI request is executed'
+ def response = mvc.perform(get("$dataNodeBaseEndpointNcmp/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=stores:bookstore/categories=100"))
+ .andReturn().response
+ then: 'NCMP service responds with 502 Bad Gateway status'
+ response.status == HttpStatus.BAD_GATEWAY.value()
+ and: 'the NCMP response also contains the original DMI response details'
+ response.contentAsString.contains('400')
+ response.contentAsString.contains('Bad Request from DMI')
+ }
+
def setupTestException(exception, apiType) {
if (NCMP == apiType) {
mockNetworkCmProxyDataService.getYangResourcesModuleReferences(*_) >> { throw exception }
diff --git a/cps-ncmp-rest/src/test/resources/dmi_registration_all_singing_and_dancing.json b/cps-ncmp-rest/src/test/resources/dmi_registration_all_singing_and_dancing.json
index fd8b56b02..c2a307db2 100644
--- a/cps-ncmp-rest/src/test/resources/dmi_registration_all_singing_and_dancing.json
+++ b/cps-ncmp-rest/src/test/resources/dmi_registration_all_singing_and_dancing.json
@@ -3,7 +3,7 @@
"dmiModelPlugin":"service3",
"createdCmHandles":[
{
- "cmHandle":"ch1(new)",
+ "cmHandle":"ch1-new",
"cmHandleProperties":{
"dmiProp1":"ch1-dmi1",
"dmiProp2":"ch1-dmi2"
@@ -14,7 +14,7 @@
}
},
{
- "cmHandle":"ch2(new)",
+ "cmHandle":"ch2-new",
"cmHandleProperties":{
"dmiProp1":"ch2-dmi1",
"dmiProp2":"ch2-dmi2"
@@ -27,7 +27,7 @@
],
"updatedCmHandles":[
{
- "cmHandle":"ch3(upd)",
+ "cmHandle":"ch3-upd",
"cmHandleProperties":{
"dmiProp1":"ch3-dmi1"
},
diff --git a/cps-ncmp-rest/src/test/resources/dmi_registration_updates_only.json b/cps-ncmp-rest/src/test/resources/dmi_registration_updates_only.json
index 58a1a9836..26acdbdcb 100644
--- a/cps-ncmp-rest/src/test/resources/dmi_registration_updates_only.json
+++ b/cps-ncmp-rest/src/test/resources/dmi_registration_updates_only.json
@@ -2,7 +2,7 @@
"dmiPlugin": "service1",
"updatedCmHandles":[
{
- "cmHandle":"ch3(upd)",
+ "cmHandle":"ch3-upd",
"cmHandleProperties":{
"dmiProp1":"ch3-dmi1",
"dmiProp2":null
diff --git a/cps-ncmp-rest/src/test/resources/dmi_registration_without_properties.json b/cps-ncmp-rest/src/test/resources/dmi_registration_without_properties.json
index 395c098d2..a5dd7b0aa 100644
--- a/cps-ncmp-rest/src/test/resources/dmi_registration_without_properties.json
+++ b/cps-ncmp-rest/src/test/resources/dmi_registration_without_properties.json
@@ -4,7 +4,7 @@
"dmiModelPlugin":"service3",
"createdCmHandles":[
{
- "cmHandle": "ch1(new)"
+ "cmHandle": "ch1-new"
}
]
}
diff --git a/cps-ncmp-service/pom.xml b/cps-ncmp-service/pom.xml
index fe061eaed..573c76e4a 100644
--- a/cps-ncmp-service/pom.xml
+++ b/cps-ncmp-service/pom.xml
@@ -26,7 +26,7 @@
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java
index d942d26c8..058c42b7b 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java
@@ -3,6 +3,7 @@
* Copyright (C) 2021 highstreet technologies GmbH
* Modifications Copyright (C) 2021-2022 Nordix Foundation
* Modifications Copyright (C) 2021 Pantheon.tech
+ * Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,7 +26,10 @@ package org.onap.cps.ncmp.api;
import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum;
import java.util.Collection;
+import java.util.Set;
+import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters;
import org.onap.cps.ncmp.api.models.DmiPluginRegistration;
+import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse;
import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
import org.onap.cps.spi.model.ModuleReference;
@@ -38,8 +42,9 @@ public interface NetworkCmProxyDataService {
* Registration of New CM Handles.
*
* @param dmiPluginRegistration Dmi Plugin Registration
+ * @return dmiPluginRegistrationResponse
*/
- void updateDmiRegistrationAndSyncModule(DmiPluginRegistration dmiPluginRegistration);
+ DmiPluginRegistrationResponse updateDmiRegistrationAndSyncModule(DmiPluginRegistration dmiPluginRegistration);
/**
* Get resource data for data store pass-through operational
@@ -47,16 +52,16 @@ public interface NetworkCmProxyDataService {
*
* @param cmHandleId cm handle identifier
* @param resourceIdentifier resource identifier
- * @param acceptParamInHeader accept param
* @param optionsParamInQuery options query
* @param topicParamInQuery topic name for (triggering) async responses
+ * @param requestId unique requestId for async request
* @return {@code Object} resource data
*/
Object getResourceDataOperationalForCmHandle(String cmHandleId,
String resourceIdentifier,
- String acceptParamInHeader,
String optionsParamInQuery,
- String topicParamInQuery);
+ String topicParamInQuery,
+ String requestId);
/**
* Get resource data for data store pass-through running
@@ -64,16 +69,16 @@ public interface NetworkCmProxyDataService {
*
* @param cmHandleId cm handle identifier
* @param resourceIdentifier resource identifier
- * @param acceptParamInHeader accept param
* @param optionsParamInQuery options query
- * @param topicParamInQuery topic query
+ * @param topicParamInQuery topic name for (triggering) async responses
+ * @param requestId unique requestId for async request
* @return {@code Object} resource data
*/
Object getResourceDataPassThroughRunningForCmHandle(String cmHandleId,
String resourceIdentifier,
- String acceptParamInHeader,
String optionsParamInQuery,
- String topicParamInQuery);
+ String topicParamInQuery,
+ String requestId);
/**
* Write resource data for data store pass-through running
@@ -116,4 +121,11 @@ public interface NetworkCmProxyDataService {
*/
NcmpServiceCmHandle getNcmpServiceCmHandle(String cmHandleId);
+ /**
+ * Query and return cm handles that match the given query parameters.
+ *
+ * @param cmHandleQueryApiParameters the cm handle query parameters
+ * @return collection of cm handle ids
+ */
+ Set<String> queryCmHandles(CmHandleQueryApiParameters cmHandleQueryApiParameters);
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
index 76d4cef9e..e624953f5 100755
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
@@ -3,7 +3,7 @@
* Copyright (C) 2021 highstreet technologies GmbH
* Modifications Copyright (C) 2021-2022 Nordix Foundation
* Modifications Copyright (C) 2021 Pantheon.tech
- * Modifications Copyright (C) 2021 Bell Canada
+ * Modifications Copyright (C) 2021-2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,14 +31,13 @@ import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMES
import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum;
import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED;
-import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.base.Strings;
+import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.UUID;
-import java.util.regex.Pattern;
+import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -46,21 +45,25 @@ import org.onap.cps.api.CpsAdminService;
import org.onap.cps.api.CpsDataService;
import org.onap.cps.api.CpsModuleService;
import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
-import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException;
-import org.onap.cps.ncmp.api.impl.exception.ServerNcmpException;
+import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException;
import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations;
-import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations;
import org.onap.cps.ncmp.api.impl.operations.DmiOperations;
import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever;
import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
-import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandlesList;
+import org.onap.cps.ncmp.api.inventory.sync.ModuleSyncService;
+import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError;
import org.onap.cps.ncmp.api.models.DmiPluginRegistration;
+import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse;
import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
+import org.onap.cps.spi.exceptions.AlreadyDefinedException;
import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
import org.onap.cps.spi.exceptions.DataValidationException;
+import org.onap.cps.spi.exceptions.SchemaSetNotFoundException;
import org.onap.cps.spi.model.ModuleReference;
+import org.onap.cps.utils.CpsValidator;
import org.onap.cps.utils.JsonObjectMapper;
-import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
@@ -75,8 +78,6 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
private final DmiDataOperations dmiDataOperations;
- private final DmiModelOperations dmiModelOperations;
-
private final CpsModuleService cpsModuleService;
private final CpsAdminService cpsAdminService;
@@ -85,50 +86,47 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
private final YangModelCmHandleRetriever yangModelCmHandleRetriever;
- // valid kafka topic name regex
- private static final Pattern TOPIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9]([._-](?![._-])|"
- + "[a-zA-Z0-9]){0,120}[a-zA-Z0-9]$");
- private static final String NO_REQUEST_ID = null;
- private static final String NO_TOPIC = null;
+ private final ModuleSyncService moduleSyncService;
@Override
- public void updateDmiRegistrationAndSyncModule(final DmiPluginRegistration dmiPluginRegistration) {
+ public DmiPluginRegistrationResponse updateDmiRegistrationAndSyncModule(
+ final DmiPluginRegistration dmiPluginRegistration) {
dmiPluginRegistration.validateDmiPluginRegistration();
- try {
- if (!dmiPluginRegistration.getCreatedCmHandles().isEmpty()) {
- parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration);
- }
- if (!dmiPluginRegistration.getUpdatedCmHandles().isEmpty()) {
- parseAndUpdateCmHandlesInDmiRegistration(dmiPluginRegistration);
- }
- parseAndRemoveCmHandlesInDmiRegistration(dmiPluginRegistration);
- } catch (final JsonProcessingException | DataNodeNotFoundException e) {
- final String errorMessage = String.format(
- "Error occurred while processing the CM-handle registration request, caused by : [%s]",
- e.getMessage());
- throw new DataValidationException(errorMessage, e.getMessage(), e);
+ final var dmiPluginRegistrationResponse = new DmiPluginRegistrationResponse();
+ dmiPluginRegistrationResponse.setRemovedCmHandles(
+ parseAndRemoveCmHandlesInDmiRegistration(dmiPluginRegistration.getRemovedCmHandles()));
+ if (!dmiPluginRegistration.getCreatedCmHandles().isEmpty()) {
+ dmiPluginRegistrationResponse.setCreatedCmHandles(
+ parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration));
+ }
+ if (!dmiPluginRegistration.getUpdatedCmHandles().isEmpty()) {
+ dmiPluginRegistrationResponse.setUpdatedCmHandles(
+ networkCmProxyDataServicePropertyHandler
+ .updateCmHandleProperties(dmiPluginRegistration.getUpdatedCmHandles()));
}
+ return dmiPluginRegistrationResponse;
}
@Override
public Object getResourceDataOperationalForCmHandle(final String cmHandleId,
final String resourceIdentifier,
- final String acceptParamInHeader,
final String optionsParamInQuery,
- final String topicParamInQuery) {
-
- return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, acceptParamInHeader,
- DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, optionsParamInQuery, topicParamInQuery);
+ final String topicParamInQuery,
+ final String requestId) {
+ CpsValidator.validateNameCharacters(cmHandleId);
+ return getResourceDataResponse(cmHandleId, resourceIdentifier,
+ DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, optionsParamInQuery, topicParamInQuery, requestId);
}
@Override
public Object getResourceDataPassThroughRunningForCmHandle(final String cmHandleId,
final String resourceIdentifier,
- final String acceptParamInHeader,
final String optionsParamInQuery,
- final String topicParamInQuery) {
- return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, acceptParamInHeader,
- DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING, optionsParamInQuery, topicParamInQuery);
+ final String topicParamInQuery,
+ final String requestId) {
+ CpsValidator.validateNameCharacters(cmHandleId);
+ return getResourceDataResponse(cmHandleId, resourceIdentifier,
+ DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING, optionsParamInQuery, topicParamInQuery, requestId);
}
@Override
@@ -137,15 +135,16 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
final OperationEnum operation,
final String requestData,
final String dataType) {
+ CpsValidator.validateNameCharacters(cmHandleId);
return handleResponse(
- dmiDataOperations.writeResourceDataPassThroughRunningFromDmi(
- cmHandleId, resourceIdentifier, operation, requestData, dataType),
- "Not able to " + operation + " resource data.");
+ dmiDataOperations.writeResourceDataPassThroughRunningFromDmi(cmHandleId, resourceIdentifier, operation,
+ requestData, dataType), operation);
}
@Override
public Collection<ModuleReference> getYangResourcesModuleReferences(final String cmHandleId) {
+ CpsValidator.validateNameCharacters(cmHandleId);
return cpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId);
}
@@ -160,180 +159,172 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
return cpsAdminService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, moduleNames);
}
+ @Override
+ public Set<String> queryCmHandles(final CmHandleQueryApiParameters cmHandleQueryApiParameters) {
+
+ cmHandleQueryApiParameters.getPublicProperties().forEach((key, value) -> {
+ if (Strings.isNullOrEmpty(key)) {
+ throw new DataValidationException("Invalid Query Parameter.",
+ "Missing property name - please supply a valid name.");
+ }
+ });
+
+ return cpsAdminService.queryCmHandles(jsonObjectMapper.convertToValueType(cmHandleQueryApiParameters,
+ org.onap.cps.spi.model.CmHandleQueryParameters.class));
+ }
+
/**
* Retrieve cm handle details for a given cm handle.
+ *
* @param cmHandleId cm handle identifier
* @return cm handle details
*/
@Override
public NcmpServiceCmHandle getNcmpServiceCmHandle(final String cmHandleId) {
+ CpsValidator.validateNameCharacters(cmHandleId);
final NcmpServiceCmHandle ncmpServiceCmHandle = new NcmpServiceCmHandle();
final YangModelCmHandle yangModelCmHandle =
yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId);
final List<YangModelCmHandle.Property> dmiProperties = yangModelCmHandle.getDmiProperties();
final List<YangModelCmHandle.Property> publicProperties = yangModelCmHandle.getPublicProperties();
- ncmpServiceCmHandle.setCmHandleID(yangModelCmHandle.getId());
+ ncmpServiceCmHandle.setCmHandleId(yangModelCmHandle.getId());
setDmiProperties(dmiProperties, ncmpServiceCmHandle);
setPublicProperties(publicProperties, ncmpServiceCmHandle);
return ncmpServiceCmHandle;
}
- private void setDmiProperties(final List<YangModelCmHandle.Property> dmiProperties,
- final NcmpServiceCmHandle ncmpServiceCmHandle) {
- final Map<String, String> dmiPropertiesMap = new LinkedHashMap<>(dmiProperties.size());
- asPropertiesMap(dmiProperties, dmiPropertiesMap);
- ncmpServiceCmHandle.setDmiProperties(dmiPropertiesMap);
- }
-
- private void setPublicProperties(final List<YangModelCmHandle.Property> publicProperties,
- final NcmpServiceCmHandle ncmpServiceCmHandle) {
- final Map<String, String> publicPropertiesMap = new LinkedHashMap<>();
- asPropertiesMap(publicProperties, publicPropertiesMap);
- ncmpServiceCmHandle.setPublicProperties(publicPropertiesMap);
- }
-
- private void asPropertiesMap(final List<YangModelCmHandle.Property> properties,
- final Map<String, String> propertiesMap) {
- for (final YangModelCmHandle.Property property: properties) {
- propertiesMap.put(property.getName(), property.getValue());
- }
- }
-
/**
* THis method registers a cm handle and initiates modules sync.
*
* @param dmiPluginRegistration dmi plugin registration information.
- * @throws JsonProcessingException thrown if json is malformed or missing.
+ * @return cm-handle registration response for create cm-handle requests.
*/
- public void parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(
- final DmiPluginRegistration dmiPluginRegistration) throws JsonProcessingException {
- final YangModelCmHandlesList createdYangModelCmHandlesList =
- getUpdatedYangModelCmHandlesList(dmiPluginRegistration,
- dmiPluginRegistration.getCreatedCmHandles());
- registerAndSyncNewCmHandles(createdYangModelCmHandlesList);
- }
-
- private static Object handleResponse(final ResponseEntity<?> responseEntity,
- final String exceptionMessage) {
- if (responseEntity.getStatusCode().is2xxSuccessful()) {
- return responseEntity.getBody();
- } else {
- throw new ServerNcmpException(exceptionMessage,
- "DMI status code: " + responseEntity.getStatusCodeValue()
- + ", DMI response body: " + responseEntity.getBody());
- }
- }
-
- private void parseAndUpdateCmHandlesInDmiRegistration(final DmiPluginRegistration dmiPluginRegistration) {
- networkCmProxyDataServicePropertyHandler.updateCmHandleProperties(dmiPluginRegistration.getUpdatedCmHandles());
- }
-
- private YangModelCmHandlesList getUpdatedYangModelCmHandlesList(
- final DmiPluginRegistration dmiPluginRegistration,
- final List<NcmpServiceCmHandle> updatedCmHandles) {
- return YangModelCmHandlesList.toYangModelCmHandlesList(
- dmiPluginRegistration.getDmiPlugin(),
- dmiPluginRegistration.getDmiDataPlugin(),
- dmiPluginRegistration.getDmiModelPlugin(),
- updatedCmHandles);
- }
-
- private void registerAndSyncNewCmHandles(final YangModelCmHandlesList yangModelCmHandlesList) {
- final String cmHandleJsonData = jsonObjectMapper.asJsonString(yangModelCmHandlesList);
- cpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
- cmHandleJsonData, NO_TIMESTAMP);
-
- for (final YangModelCmHandle yangModelCmHandle : yangModelCmHandlesList.getYangModelCmHandles()) {
- syncModulesAndCreateAnchor(yangModelCmHandle);
+ public List<CmHandleRegistrationResponse> parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(
+ final DmiPluginRegistration dmiPluginRegistration) {
+ List<CmHandleRegistrationResponse> cmHandleRegistrationResponses = new ArrayList<>();
+ try {
+ cmHandleRegistrationResponses = dmiPluginRegistration.getCreatedCmHandles().stream()
+ .map(cmHandle ->
+ YangModelCmHandle.toYangModelCmHandle(
+ dmiPluginRegistration.getDmiPlugin(),
+ dmiPluginRegistration.getDmiDataPlugin(),
+ dmiPluginRegistration.getDmiModelPlugin(), cmHandle)
+ )
+ .map(this::registerAndSyncNewCmHandle)
+ .collect(Collectors.toList());
+ } catch (final DataValidationException dataValidationException) {
+ cmHandleRegistrationResponses.add(CmHandleRegistrationResponse.createFailureResponse(dmiPluginRegistration
+ .getCreatedCmHandles().stream()
+ .map(NcmpServiceCmHandle::getCmHandleId).findFirst().orElse(null),
+ RegistrationError.CM_HANDLE_INVALID_ID));
}
+ return cmHandleRegistrationResponses;
}
protected void syncModulesAndCreateAnchor(final YangModelCmHandle yangModelCmHandle) {
- syncAndCreateSchemaSet(yangModelCmHandle);
- createAnchor(yangModelCmHandle);
+ final String schemaSetName = moduleSyncService.syncAndCreateSchemaSet(yangModelCmHandle);
+ final String anchorName = yangModelCmHandle.getId();
+ cpsAdminService.createAnchor(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, schemaSetName,
+ anchorName);
}
- private void parseAndRemoveCmHandlesInDmiRegistration(final DmiPluginRegistration dmiPluginRegistration) {
- for (final String cmHandle : dmiPluginRegistration.getRemovedCmHandles()) {
+ protected List<CmHandleRegistrationResponse> parseAndRemoveCmHandlesInDmiRegistration(
+ final List<String> tobeRemovedCmHandles) {
+ final List<CmHandleRegistrationResponse> cmHandleRegistrationResponses =
+ new ArrayList<>(tobeRemovedCmHandles.size());
+ for (final String cmHandle : tobeRemovedCmHandles) {
try {
- attemptToDeleteSchemaSetWithCascade(cmHandle);
+ CpsValidator.validateNameCharacters(cmHandle);
+ deleteSchemaSetWithCascade(cmHandle);
cpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
"/dmi-registry/cm-handles[@id='" + cmHandle + "']", NO_TIMESTAMP);
- } catch (final DataNodeNotFoundException e) {
- log.warn("Datanode {} not deleted message {}", cmHandle, e.getMessage());
+ cmHandleRegistrationResponses.add(CmHandleRegistrationResponse.createSuccessResponse(cmHandle));
+ } catch (final DataNodeNotFoundException dataNodeNotFoundException) {
+ log.error("Unable to find dataNode for cmHandleId : {} , caused by : {}",
+ cmHandle, dataNodeNotFoundException.getMessage());
+ cmHandleRegistrationResponses.add(CmHandleRegistrationResponse
+ .createFailureResponse(cmHandle, RegistrationError.CM_HANDLE_DOES_NOT_EXIST));
+ } catch (final DataValidationException dataValidationException) {
+ log.error("Unable to de-register cm-handle id: {}, caused by: {}",
+ cmHandle, dataValidationException.getMessage());
+ cmHandleRegistrationResponses.add(CmHandleRegistrationResponse
+ .createFailureResponse(cmHandle, RegistrationError.CM_HANDLE_INVALID_ID));
+ } catch (final Exception exception) {
+ log.error("Unable to de-register cm-handle id : {} , caused by : {}",
+ cmHandle, exception.getMessage());
+ cmHandleRegistrationResponses.add(
+ CmHandleRegistrationResponse.createFailureResponse(cmHandle, exception));
}
}
+ return cmHandleRegistrationResponses;
}
- private void attemptToDeleteSchemaSetWithCascade(final String schemaSetName) {
+ private void deleteSchemaSetWithCascade(final String schemaSetName) {
try {
cpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, schemaSetName,
CASCADE_DELETE_ALLOWED);
- } catch (final Exception e) {
- log.warn("Schema set {} delete failed, reason {}", schemaSetName, e.getMessage());
+ } catch (final SchemaSetNotFoundException schemaSetNotFoundException) {
+ log.warn("Schema set {} does not exist or already deleted", schemaSetName);
}
}
- private void syncAndCreateSchemaSet(final YangModelCmHandle yangModelCmHandle) {
- final Collection<ModuleReference> moduleReferencesFromCmHandle =
- dmiModelOperations.getModuleReferences(yangModelCmHandle);
-
- final Collection<ModuleReference> identifiedNewModuleReferencesFromCmHandle = cpsModuleService
- .identifyNewModuleReferences(moduleReferencesFromCmHandle);
-
- final Collection<ModuleReference> existingModuleReferencesFromCmHandle =
- moduleReferencesFromCmHandle.stream().filter(moduleReferenceFromCmHandle ->
- !identifiedNewModuleReferencesFromCmHandle.contains(moduleReferenceFromCmHandle)
- ).collect(Collectors.toList());
+ private Object getResourceDataResponse(final String cmHandleId,
+ final String resourceIdentifier,
+ final DmiOperations.DataStoreEnum dataStore,
+ final String optionsParamInQuery,
+ final String topicParamInQuery,
+ final String requestId) {
+ final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi(
+ cmHandleId, resourceIdentifier, optionsParamInQuery, dataStore, requestId, topicParamInQuery);
+ return handleResponse(responseEntity, OperationEnum.READ);
+ }
- final Map<String, String> newModuleNameToContentMap;
- if (identifiedNewModuleReferencesFromCmHandle.isEmpty()) {
- newModuleNameToContentMap = new HashMap<>();
- } else {
- newModuleNameToContentMap = dmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle,
- identifiedNewModuleReferencesFromCmHandle);
- }
- cpsModuleService
- .createSchemaSetFromModules(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, yangModelCmHandle.getId(),
- newModuleNameToContentMap, existingModuleReferencesFromCmHandle);
+ private void setDmiProperties(final List<YangModelCmHandle.Property> dmiProperties,
+ final NcmpServiceCmHandle ncmpServiceCmHandle) {
+ final Map<String, String> dmiPropertiesMap = new LinkedHashMap<>(dmiProperties.size());
+ asPropertiesMap(dmiProperties, dmiPropertiesMap);
+ ncmpServiceCmHandle.setDmiProperties(dmiPropertiesMap);
}
- private void createAnchor(final YangModelCmHandle yangModelCmHandle) {
- cpsAdminService.createAnchor(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, yangModelCmHandle.getId(),
- yangModelCmHandle.getId());
+ private void setPublicProperties(final List<YangModelCmHandle.Property> publicProperties,
+ final NcmpServiceCmHandle ncmpServiceCmHandle) {
+ final Map<String, String> publicPropertiesMap = new LinkedHashMap<>();
+ asPropertiesMap(publicProperties, publicPropertiesMap);
+ ncmpServiceCmHandle.setPublicProperties(publicPropertiesMap);
}
- private static boolean hasTopicParameter(final String topicName) {
- if (topicName == null) {
- return false;
- }
- if (TOPIC_NAME_PATTERN.matcher(topicName).matches()) {
- return true;
+ private void asPropertiesMap(final List<YangModelCmHandle.Property> properties,
+ final Map<String, String> propertiesMap) {
+ for (final YangModelCmHandle.Property property: properties) {
+ propertiesMap.put(property.getName(), property.getValue());
}
- throw new InvalidTopicException("Topic name " + topicName + " is invalid", "invalid topic");
}
- private Map<String, Object> buildDmiResponse(final String requestId) {
- final Map<String, Object> dmiResponseMap = new HashMap<>();
- dmiResponseMap.put("requestId", requestId);
- return dmiResponseMap;
+
+ private CmHandleRegistrationResponse registerAndSyncNewCmHandle(final YangModelCmHandle yangModelCmHandle) {
+ try {
+ final String cmHandleJsonData = String.format("{\"cm-handles\":[%s]}",
+ jsonObjectMapper.asJsonString(yangModelCmHandle));
+ cpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
+ cmHandleJsonData, NO_TIMESTAMP);
+ syncModulesAndCreateAnchor(yangModelCmHandle);
+ return CmHandleRegistrationResponse.createSuccessResponse(yangModelCmHandle.getId());
+ } catch (final AlreadyDefinedException alreadyDefinedException) {
+ return CmHandleRegistrationResponse.createFailureResponse(
+ yangModelCmHandle.getId(), RegistrationError.CM_HANDLE_ALREADY_EXIST);
+ } catch (final Exception exception) {
+ return CmHandleRegistrationResponse.createFailureResponse(yangModelCmHandle.getId(), exception);
+ }
}
- private Object validateTopicNameAndGetResourceData(final String cmHandleId,
- final String resourceIdentifier,
- final String acceptParamInHeader,
- final DmiOperations.DataStoreEnum dataStore,
- final String optionsParamInQuery,
- final String topicParamInQuery) {
- final boolean processAsynchronously = hasTopicParameter(topicParamInQuery);
- if (processAsynchronously) {
- final String resourceDataRequestId = UUID.randomUUID().toString();
- return ResponseEntity.status(HttpStatus.OK)
- .body(buildDmiResponse(resourceDataRequestId));
+ private static Object handleResponse(final ResponseEntity<?> responseEntity, final OperationEnum operation) {
+ if (responseEntity.getStatusCode().is2xxSuccessful()) {
+ return responseEntity.getBody();
+ } else {
+ final String exceptionMessage = "Unable to " + operation.toString() + " resource data.";
+ throw new HttpClientRequestException(exceptionMessage, (String) responseEntity.getBody(),
+ responseEntity.getStatusCodeValue());
}
- final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi(
- cmHandleId, resourceIdentifier, optionsParamInQuery, acceptParamInHeader,
- dataStore, NO_REQUEST_ID, NO_TOPIC);
- return handleResponse(responseEntity, "Not able to get resource data.");
}
+
} \ No newline at end of file
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java
index ca2f578f4..aae2f209a 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2022 Nordix Foundation
+ * Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,20 +29,26 @@ import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NCMP_DMI
import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMESTAMP;
import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.api.CpsDataService;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError;
import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
+import org.onap.cps.spi.exceptions.DataValidationException;
import org.onap.cps.spi.model.DataNode;
import org.onap.cps.spi.model.DataNodeBuilder;
+import org.onap.cps.utils.CpsValidator;
import org.springframework.stereotype.Service;
@Slf4j
@@ -61,23 +68,38 @@ public class NetworkCmProxyDataServicePropertyHandler {
*
* @param ncmpServiceCmHandles collection of ncmpServiceCmHandles
*/
- public void updateCmHandleProperties(final Collection<NcmpServiceCmHandle> ncmpServiceCmHandles)
- throws DataNodeNotFoundException {
+ public List<CmHandleRegistrationResponse> updateCmHandleProperties(
+ final Collection<NcmpServiceCmHandle> ncmpServiceCmHandles) {
+ final List<CmHandleRegistrationResponse> cmHandleRegistrationResponses = new ArrayList<>();
for (final NcmpServiceCmHandle ncmpServiceCmHandle : ncmpServiceCmHandles) {
+ final String cmHandle = ncmpServiceCmHandle.getCmHandleId();
try {
- final String cmHandleXpath = String.format(CM_HANDLE_XPATH_TEMPLATE,
- ncmpServiceCmHandle.getCmHandleID());
+ CpsValidator.validateNameCharacters(cmHandle);
+ final String cmHandleXpath = String.format(CM_HANDLE_XPATH_TEMPLATE, cmHandle);
final DataNode existingCmHandleDataNode =
cpsDataService.getDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandleXpath,
FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS);
processUpdates(existingCmHandleDataNode, ncmpServiceCmHandle);
+ cmHandleRegistrationResponses.add(CmHandleRegistrationResponse.createSuccessResponse(cmHandle));
} catch (final DataNodeNotFoundException e) {
log.error("Unable to find dataNode for cmHandleId : {} , caused by : {}",
- ncmpServiceCmHandle.getCmHandleID(),
- e.getMessage());
- throw e;
+ cmHandle, e.getMessage());
+ cmHandleRegistrationResponses.add(CmHandleRegistrationResponse
+ .createFailureResponse(cmHandle, RegistrationError.CM_HANDLE_DOES_NOT_EXIST));
+ } catch (final DataValidationException e) {
+ log.error("Unable to update cm handle : {}, caused by : {}",
+ cmHandle, e.getMessage());
+ cmHandleRegistrationResponses.add(
+ CmHandleRegistrationResponse.createFailureResponse(cmHandle,
+ RegistrationError.CM_HANDLE_INVALID_ID));
+ } catch (final Exception exception) {
+ log.error("Unable to update cmHandle : {} , caused by : {}",
+ cmHandle, exception.getMessage());
+ cmHandleRegistrationResponses.add(
+ CmHandleRegistrationResponse.createFailureResponse(cmHandle, exception));
}
}
+ return cmHandleRegistrationResponses;
}
private void processUpdates(final DataNode existingCmHandleDataNode, final NcmpServiceCmHandle incomingCmHandle) {
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java
index 94faa557f..f1bb95f34 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2021 Nordix Foundation
+ * Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,43 +21,32 @@
package org.onap.cps.ncmp.api.impl.client;
+import lombok.AllArgsConstructor;
import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration.DmiProperties;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
+@AllArgsConstructor
public class DmiRestClient {
private RestTemplate restTemplate;
private DmiProperties dmiProperties;
- /**
- * Constructor injection for DmiRestClient objects.
- *
- * @param restTemplate the rest template
- * @param dmiProperties the DMI properties
- */
- public DmiRestClient(final RestTemplate restTemplate, final DmiProperties dmiProperties) {
- this.restTemplate = restTemplate;
- this.dmiProperties = dmiProperties;
- }
/**
* Sends POST operation to DMI with json body containing module references.
* @param dmiResourceUrl dmi resource url
* @param jsonData json data body
- * @param httpHeaders http headers
* @return response entity of type String
*/
public ResponseEntity<Object> postOperationWithJsonData(final String dmiResourceUrl,
- final String jsonData,
- final HttpHeaders httpHeaders) {
- final var httpEntity = new HttpEntity<>(jsonData, configureHttpHeaders(httpHeaders));
+ final String jsonData) {
+ final var httpEntity = new HttpEntity<>(jsonData, configureHttpHeaders(new HttpHeaders()));
return restTemplate.postForEntity(dmiResourceUrl, httpEntity, Object.class);
}
@@ -65,15 +55,4 @@ public class DmiRestClient {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
return httpHeaders;
}
-
- /**
- * Sends POST operation to DMI.
- * @param dmiResourceUrl dmi resource url
- * @param httpHeaders http headers
- * @return response entity of type String
- */
- public ResponseEntity<Object> postOperation(final String dmiResourceUrl, final HttpHeaders httpHeaders) {
- final var httpEntity = new HttpEntity<>(configureHttpHeaders(httpHeaders));
- return restTemplate.exchange(dmiResourceUrl, HttpMethod.POST, httpEntity, Object.class);
- }
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java
new file mode 100644
index 000000000..9d307e5d2
--- /dev/null
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java
@@ -0,0 +1,45 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.exception;
+
+import lombok.Getter;
+
+/**
+ * Http Client Request exception for passthrough scenarios.
+ */
+@Getter
+public class HttpClientRequestException extends NcmpException {
+
+ private static final long serialVersionUID = 6659897770659834797L;
+ final Integer httpStatus;
+
+ /**
+ * Constructor to form exception for passthrough scenarios.
+ *
+ * @param message message details from NCMP
+ * @param details response body from the client available as details
+ * @param httpStatus http status code from the client
+ */
+ public HttpClientRequestException(final String message, final String details, final Integer httpStatus) {
+ super(message, details);
+ this.httpStatus = httpStatus;
+ }
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java
index 68de9d5c6..ad85edde7 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2021-2022 Nordix Foundation
+ * Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,8 +29,8 @@ import org.onap.cps.ncmp.api.impl.client.DmiRestClient;
import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration;
import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder;
import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
+import org.onap.cps.utils.CpsValidator;
import org.onap.cps.utils.JsonObjectMapper;
-import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
@@ -58,7 +59,6 @@ public class DmiDataOperations extends DmiOperations {
* @param cmHandleId network resource identifier
* @param resourceId resource identifier
* @param optionsParamInQuery options query
- * @param acceptParamInHeader accept parameter
* @param dataStore data store enum
* @param requestId requestId for async responses
* @param topicParamInQuery topic name for (triggering) async responses
@@ -67,10 +67,10 @@ public class DmiDataOperations extends DmiOperations {
public ResponseEntity<Object> getResourceDataFromDmi(final String cmHandleId,
final String resourceId,
final String optionsParamInQuery,
- final String acceptParamInHeader,
final DataStoreEnum dataStore,
final String requestId,
final String topicParamInQuery) {
+ CpsValidator.validateNameCharacters(cmHandleId);
final YangModelCmHandle yangModelCmHandle =
yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId);
final DmiRequestBody dmiRequestBody = DmiRequestBody.builder()
@@ -79,13 +79,11 @@ public class DmiDataOperations extends DmiOperations {
.build();
dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties());
final String jsonBody = jsonObjectMapper.asJsonString(dmiRequestBody);
-
- final var dmiResourceDataUrl = dmiServiceUrlBuilder.getDmiDatastoreUrl(
+ final String dmiResourceDataUrl = dmiServiceUrlBuilder.getDmiDatastoreUrl(
dmiServiceUrlBuilder.populateQueryParams(resourceId, optionsParamInQuery,
topicParamInQuery), dmiServiceUrlBuilder.populateUriVariables(
yangModelCmHandle, cmHandleId, dataStore));
- final var httpHeaders = prepareHeader(acceptParamInHeader);
- return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonBody, httpHeaders);
+ return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonBody);
}
/**
@@ -104,6 +102,7 @@ public class DmiDataOperations extends DmiOperations {
final OperationEnum operation,
final String requestData,
final String dataType) {
+ CpsValidator.validateNameCharacters(cmHandleId);
final YangModelCmHandle yangModelCmHandle =
yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId);
final DmiRequestBody dmiRequestBody = DmiRequestBody.builder()
@@ -114,10 +113,10 @@ public class DmiDataOperations extends DmiOperations {
dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties());
final String jsonBody = jsonObjectMapper.asJsonString(dmiRequestBody);
final String dmiUrl =
- dmiServiceUrlBuilder.getDmiDatastoreUrl(dmiServiceUrlBuilder.populateQueryParams(resourceId,
- null, null),
- dmiServiceUrlBuilder.populateUriVariables(yangModelCmHandle, cmHandleId, PASSTHROUGH_RUNNING));
- return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonBody, new HttpHeaders());
+ dmiServiceUrlBuilder.getDmiDatastoreUrl(dmiServiceUrlBuilder.populateQueryParams(resourceId,
+ null, null),
+ dmiServiceUrlBuilder.populateUriVariables(yangModelCmHandle, cmHandleId, PASSTHROUGH_RUNNING));
+ return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonBody);
}
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java
index d79988e2e..b033af87c 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2021-2022 Nordix Foundation
+ * Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,7 +37,6 @@ import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
import org.onap.cps.ncmp.api.models.YangResource;
import org.onap.cps.spi.model.ModuleReference;
import org.onap.cps.utils.JsonObjectMapper;
-import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
@@ -107,7 +107,7 @@ public class DmiModelOperations extends DmiOperations {
final String cmHandle,
final String resourceName) {
final String dmiResourceDataUrl = getDmiResourceUrl(dmiServiceName, cmHandle, resourceName);
- return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonData, new HttpHeaders());
+ return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonData);
}
private static String getRequestBodyToFetchYangResources(final Collection<ModuleReference> newModuleReferences,
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java
index 75ba91b4f..745007bd4 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2021-2022 Nordix Foundation
+ * Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +27,6 @@ import org.onap.cps.ncmp.api.impl.client.DmiRestClient;
import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration;
import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder;
import org.onap.cps.utils.JsonObjectMapper;
-import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@@ -56,10 +56,5 @@ public class DmiOperations {
.buildAndExpand(dmiServiceName, dmiProperties.getDmiBasePath(), cmHandle, resourceName).toUriString();
}
- static HttpHeaders prepareHeader(final String acceptParam) {
- final var httpHeaders = new HttpHeaders();
- httpHeaders.set(HttpHeaders.ACCEPT, acceptParam);
- return httpHeaders;
- }
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetriever.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetriever.java
index 6b6bdf5be..0efe8d5b6 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetriever.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetriever.java
@@ -28,6 +28,7 @@ import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.utils.CpsValidator;
import org.springframework.stereotype.Component;
/**
@@ -48,9 +49,10 @@ public class YangModelCmHandleRetriever {
* @return yang model cm handle
*/
public YangModelCmHandle getDmiServiceNamesAndProperties(final String cmHandleId) {
+ CpsValidator.validateNameCharacters(cmHandleId);
final DataNode cmHandleDataNode = getCmHandleDataNode(cmHandleId);
final NcmpServiceCmHandle ncmpServiceCmHandle = new NcmpServiceCmHandle();
- ncmpServiceCmHandle.setCmHandleID(cmHandleId);
+ ncmpServiceCmHandle.setCmHandleId(cmHandleId);
populateCmHandleProperties(cmHandleDataNode, ncmpServiceCmHandle);
return YangModelCmHandle.toYangModelCmHandle(
String.valueOf(cmHandleDataNode.getLeaves().get("dmi-service-name")),
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java
index b60aac951..b67910725 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java
@@ -30,6 +30,7 @@ import org.apache.logging.log4j.util.TriConsumer;
import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration;
import org.onap.cps.ncmp.api.impl.operations.DmiOperations;
import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
+import org.onap.cps.utils.CpsValidator;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -70,25 +71,26 @@ public class DmiServiceUrlBuilder {
.pathSegment("{dmiBasePath}")
.pathSegment("v1")
.pathSegment("ch")
- .pathSegment("{cmHandle}");
+ .pathSegment("{cmHandleId}");
}
/**
* This method populates uri variables.
*
* @param yangModelCmHandle get dmi service name
- * @param cmHandle cm handle name for dmi registration
+ * @param cmHandleId cm handle id for dmi registration
* @return {@code String} dmi service url as string
*/
public Map<String, Object> populateUriVariables(final YangModelCmHandle yangModelCmHandle,
- final String cmHandle,
+ final String cmHandleId,
final DmiOperations.DataStoreEnum dataStore) {
+ CpsValidator.validateNameCharacters(cmHandleId);
final Map<String, Object> uriVariables = new HashMap<>();
final String dmiBasePath = dmiProperties.getDmiBasePath();
uriVariables.put("dmiServiceName",
yangModelCmHandle.resolveDmiServiceName(DATA));
uriVariables.put("dmiBasePath", dmiBasePath);
- uriVariables.put("cmHandle", cmHandle);
+ uriVariables.put("cmHandleId", cmHandleId);
uriVariables.put("dataStore", dataStore.getValue());
return uriVariables;
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandle.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandle.java
index 47062b354..fd3528187 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandle.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandle.java
@@ -21,6 +21,8 @@
package org.onap.cps.ncmp.api.impl.yangmodels;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Strings;
import java.util.ArrayList;
@@ -33,6 +35,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import org.onap.cps.ncmp.api.impl.operations.RequiredDmiService;
import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
+import org.onap.cps.utils.CpsValidator;
/**
* Cm Handle which follows the Yang resource dmi registry model when persisting data to DMI or the DB.
@@ -41,6 +44,7 @@ import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
@Getter
@Setter
@NoArgsConstructor
+@JsonInclude(Include.NON_NULL)
public class YangModelCmHandle {
private String id;
@@ -72,8 +76,9 @@ public class YangModelCmHandle {
final String dmiDataServiceName,
final String dmiModelServiceName,
final NcmpServiceCmHandle ncmpServiceCmHandle) {
+ CpsValidator.validateNameCharacters(ncmpServiceCmHandle.getCmHandleId());
final YangModelCmHandle yangModelCmHandle = new YangModelCmHandle();
- yangModelCmHandle.setId(ncmpServiceCmHandle.getCmHandleID());
+ yangModelCmHandle.setId(ncmpServiceCmHandle.getCmHandleId());
yangModelCmHandle.setDmiServiceName(dmiServiceName);
yangModelCmHandle.setDmiDataServiceName(dmiDataServiceName);
yangModelCmHandle.setDmiModelServiceName(dmiModelServiceName);
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandlesList.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandlesList.java
deleted file mode 100644
index 261a0181c..000000000
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandlesList.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * ============LICENSE_START=======================================================
- * Copyright (C) 2021-2022 Nordix Foundation
- * ================================================================================
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * SPDX-License-Identifier: Apache-2.0
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.cps.ncmp.api.impl.yangmodels;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import lombok.Getter;
-import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
-
-@Getter
-public class YangModelCmHandlesList {
-
- @JsonProperty("cm-handles")
- private final List<YangModelCmHandle> yangModelCmHandles = new ArrayList<>();
-
- /**
- * Create a YangModelCmHandleList given all service names and a collection of cmHandles.
- * @param dmiServiceName the dmi service name
- * @param dmiDataServiceName the dmi data service name
- * @param dmiModelServiceName the dmi model service name
- * @param ncmpServiceCmHandles cm handles rest model
- * @return instance of YangModelCmHandleList
- */
- public static YangModelCmHandlesList toYangModelCmHandlesList(final String dmiServiceName,
- final String dmiDataServiceName,
- final String dmiModelServiceName,
- final Collection<NcmpServiceCmHandle>
- ncmpServiceCmHandles) {
- final YangModelCmHandlesList yangModelCmHandlesList = new YangModelCmHandlesList();
- for (final NcmpServiceCmHandle ncmpServiceCmHandle : ncmpServiceCmHandles) {
- final YangModelCmHandle yangModelCmHandle =
- YangModelCmHandle.toYangModelCmHandle(
- dmiServiceName,
- dmiDataServiceName,
- dmiModelServiceName,
- ncmpServiceCmHandle);
- yangModelCmHandlesList.add(yangModelCmHandle);
- }
- return yangModelCmHandlesList;
- }
-
- /**
- * Add a yangModelCmHandle.
- *
- * @param yangModelCmHandle the yangModelCmHandle to add
- */
- public void add(final YangModelCmHandle yangModelCmHandle) {
- yangModelCmHandles.add(yangModelCmHandle);
- }
-}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncService.java
new file mode 100644
index 000000000..1d00f0dc6
--- /dev/null
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncService.java
@@ -0,0 +1,84 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.inventory.sync;
+
+import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.api.CpsModuleService;
+import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations;
+import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
+import org.onap.cps.spi.model.ModuleReference;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ModuleSyncService {
+
+ private final DmiModelOperations dmiModelOperations;
+ private final CpsModuleService cpsModuleService;
+
+ /**
+ * This method registers a cm handle and initiates modules sync.
+ *
+ * @param yangModelCmHandle the yang model of cm handle.
+ * @return schemaSetName the name of the schema set (same as cm handle name).
+ */
+ public String syncAndCreateSchemaSet(final YangModelCmHandle yangModelCmHandle) {
+
+ final Collection<ModuleReference> moduleReferencesFromCmHandle =
+ dmiModelOperations.getModuleReferences(yangModelCmHandle);
+
+ final Collection<ModuleReference> identifiedNewModuleReferencesFromCmHandle = cpsModuleService
+ .identifyNewModuleReferences(moduleReferencesFromCmHandle);
+
+ final Collection<ModuleReference> existingModuleReferencesFromCmHandle =
+ moduleReferencesFromCmHandle.stream().filter(moduleReferenceFromCmHandle ->
+ !identifiedNewModuleReferencesFromCmHandle.contains(moduleReferenceFromCmHandle)
+ ).collect(Collectors.toList());
+
+ final Map<String, String> newModuleNameToContentMap;
+ if (identifiedNewModuleReferencesFromCmHandle.isEmpty()) {
+ newModuleNameToContentMap = new HashMap<>();
+ } else {
+ newModuleNameToContentMap = dmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle,
+ identifiedNewModuleReferencesFromCmHandle);
+ }
+ return createSchemaSet(yangModelCmHandle, existingModuleReferencesFromCmHandle, newModuleNameToContentMap);
+ }
+
+ private String createSchemaSet(final YangModelCmHandle yangModelCmHandle,
+ final Collection<ModuleReference> existingModuleReferencesFromCmHandle,
+ final Map<String, String> newModuleNameToContentMap) {
+ final String schemaSetName = yangModelCmHandle.getId();
+ cpsModuleService
+ .createSchemaSetFromModules(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, schemaSetName,
+ newModuleNameToContentMap, existingModuleReferencesFromCmHandle);
+ return schemaSetName;
+ }
+
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java
new file mode 100644
index 000000000..3f584ed15
--- /dev/null
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java
@@ -0,0 +1,41 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.models;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Collections;
+import java.util.Map;
+import javax.validation.Valid;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+@JsonInclude(Include.NON_NULL)
+public class CmHandleQueryApiParameters {
+
+ @JsonProperty("publicCmHandleProperties")
+ @Valid
+ private Map<String, String> publicProperties = Collections.emptyMap();
+
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java
new file mode 100644
index 000000000..1da2aa943
--- /dev/null
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java
@@ -0,0 +1,88 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Bell Canada
+ * Modifications Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.models;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+
+@Data
+@Builder
+public class CmHandleRegistrationResponse {
+
+ private final String cmHandle;
+ private final Status status;
+ private RegistrationError registrationError;
+ private String errorText;
+
+ /**
+ * Creates a failure response based on exception.
+ *
+ * @param cmHandle cmHandle
+ * @param exception exception
+ * @return CmHandleRegistrationResponse
+ */
+ public static CmHandleRegistrationResponse createFailureResponse(final String cmHandle, final Exception exception) {
+ return CmHandleRegistrationResponse.builder()
+ .cmHandle(cmHandle)
+ .status(Status.FAILURE)
+ .registrationError(RegistrationError.UNKNOWN_ERROR)
+ .errorText(exception.getMessage()).build();
+ }
+
+ /**
+ * Creates a failure response based on registration error.
+ *
+ * @param cmHandle cmHandle
+ * @param registrationError registrationError
+ * @return CmHandleRegistrationResponse
+ */
+ public static CmHandleRegistrationResponse createFailureResponse(final String cmHandle,
+ final RegistrationError registrationError) {
+ return CmHandleRegistrationResponse.builder().cmHandle(cmHandle)
+ .status(Status.FAILURE)
+ .registrationError(registrationError)
+ .errorText(registrationError.errorText)
+ .build();
+ }
+
+ public static CmHandleRegistrationResponse createSuccessResponse(final String cmHandle) {
+ return CmHandleRegistrationResponse.builder().cmHandle(cmHandle)
+ .status(Status.SUCCESS).build();
+ }
+
+ public enum Status {
+ SUCCESS, FAILURE;
+ }
+
+ @RequiredArgsConstructor
+ public enum RegistrationError {
+ UNKNOWN_ERROR("00", "Unknown error"),
+ CM_HANDLE_ALREADY_EXIST("01", "cm-handle already exists"),
+ CM_HANDLE_DOES_NOT_EXIST("02", "cm-handle does not exist"),
+ CM_HANDLE_INVALID_ID("03", "cm-handle has an invalid character(s) in id");
+
+ public final String errorCode;
+ public final String errorText;
+
+ }
+} \ No newline at end of file
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java
new file mode 100644
index 000000000..8a3d26414
--- /dev/null
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java
@@ -0,0 +1,34 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Bell Canada
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.models;
+
+import java.util.Collections;
+import java.util.List;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class DmiPluginRegistrationResponse {
+ private List<CmHandleRegistrationResponse> createdCmHandles = Collections.emptyList();
+ private List<CmHandleRegistrationResponse> updatedCmHandles = Collections.emptyList();
+ private List<CmHandleRegistrationResponse> removedCmHandles = Collections.emptyList();
+} \ No newline at end of file
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/NcmpServiceCmHandle.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/NcmpServiceCmHandle.java
index 938127020..6811b59e0 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/NcmpServiceCmHandle.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/NcmpServiceCmHandle.java
@@ -39,7 +39,7 @@ import org.springframework.validation.annotation.Validated;
@NoArgsConstructor
public class NcmpServiceCmHandle {
- private String cmHandleID;
+ private String cmHandleId;
@JsonSetter(nulls = Nulls.AS_EMPTY)
private Map<String, String> dmiProperties = Collections.emptyMap();
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy
index e410463af..5683d57e5 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2021-2022 Nordix Foundation
+ * Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,8 +21,8 @@
package org.onap.cps.ncmp.api.impl
-import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
+import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
import org.onap.cps.api.CpsAdminService
import org.onap.cps.api.CpsDataService
import org.onap.cps.api.CpsModuleService
@@ -29,20 +30,29 @@ import org.onap.cps.ncmp.api.impl.exception.DmiRequestException
import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations
import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse
import org.onap.cps.ncmp.api.models.DmiPluginRegistration
import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
+import org.onap.cps.ncmp.api.inventory.sync.ModuleSyncService
+import org.onap.cps.spi.exceptions.AlreadyDefinedException
import org.onap.cps.spi.exceptions.DataNodeNotFoundException
import org.onap.cps.spi.exceptions.DataValidationException
+import org.onap.cps.spi.exceptions.SchemaSetNotFoundException
import org.onap.cps.utils.JsonObjectMapper
import spock.lang.Shared
import spock.lang.Specification
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_DOES_NOT_EXIST
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_ALREADY_EXIST
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_INVALID_ID
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.UNKNOWN_ERROR
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status
import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED
class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
@Shared
- def ncmpServiceCmHandle = new NcmpServiceCmHandle()
+ def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'some-cm-handle-id')
@Shared
def cmHandlesArray = ['cmHandle001']
@@ -55,104 +65,57 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
def mockDmiDataOperations = Mock(DmiDataOperations)
def mockNetworkCmProxyDataServicePropertyHandler = Mock(NetworkCmProxyDataServicePropertyHandler)
def mockYangModelCmHandleRetriever = Mock(YangModelCmHandleRetriever)
+ def mockModuleSyncService = Mock(ModuleSyncService)
def noTimestamp = null
+ def objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
- def 'Register or re-register a DMI Plugin for the given cm-handle(s) with #scenario process.'() {
- given: 'a registration'
- def objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
- def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:'my-server')
- ncmpServiceCmHandle.cmHandleID = '123'
- ncmpServiceCmHandle.dmiProperties = [dmiProp1: 'dmiValue1', dmiProp2: 'dmiValue2']
- ncmpServiceCmHandle.publicProperties = [publicProp1: 'publicValue1', publicProp2: 'publicValue2' ]
- dmiPluginRegistration.createdCmHandles = createdCmHandles
- dmiPluginRegistration.updatedCmHandles = updatedCmHandles
- dmiPluginRegistration.removedCmHandles = removedCmHandles
- def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","dmi-data-service-name":null,"dmi-model-service-name":null,' +
- '"additional-properties":[{"name":"dmiProp1","value":"dmiValue1"},{"name":"dmiProp2","value":"dmiValue2"}],' +
- '"public-properties":[{"name":"publicProp1","value":"publicValue1"},{"name":"publicProp2","value":"publicValue2"}]' +
- '}]}'
- when: 'registration is updated and modules are synced'
- objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
- then: 'save list elements is invoked with the expected parameters'
- expectedCallsToSaveNode * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
- '/dmi-registry', expectedJsonData, noTimestamp)
- and: 'update data node leaves is called with correct parameters'
- expectedCallsToUpdateCmHandleProperty * mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(updatedCmHandles)
- and: 'delete schema set is invoked with the correct parameters'
- expectedCallsToDeleteSchemaSetAndListElement * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'cmHandle001', CASCADE_DELETE_ALLOWED)
- and: 'delete list or list element is invoked with the correct parameters'
- expectedCallsToDeleteSchemaSetAndListElement * mockCpsDataService.deleteListOrListElement('NCMP-Admin',
- 'ncmp-dmi-registry', "/dmi-registry/cm-handles[@id='cmHandle001']", noTimestamp)
- where:
- scenario | createdCmHandles | updatedCmHandles | removedCmHandles || expectedCallsToSaveNode | expectedCallsToDeleteSchemaSetAndListElement | expectedCallsToUpdateCmHandleProperty
- 'create' | [ncmpServiceCmHandle] | [] | [] || 1 | 0 | 0
- 'update' | [] | [ncmpServiceCmHandle] | [] || 0 | 0 | 1
- 'delete' | [] | [] | cmHandlesArray || 0 | 1 | 0
- 'create, update and delete' | [ncmpServiceCmHandle] | [ncmpServiceCmHandle] | cmHandlesArray || 1 | 1 | 1
- 'no valid data' | [] | [] | [] || 0 | 0 | 0
+ def 'DMI Registration: Create, Update & Delete operations are processed in the right order'() {
+ given: 'a registration with operations of all three types'
+ def dmiRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
+ dmiRegistration.setCreatedCmHandles([new NcmpServiceCmHandle(cmHandleId: 'cmhandle-1', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
+ dmiRegistration.setUpdatedCmHandles([new NcmpServiceCmHandle(cmHandleId: 'cmhandle-2', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
+ dmiRegistration.setRemovedCmHandles(['cmhandle-2'])
+ when: 'registration is processed'
+ objectUnderTest.updateDmiRegistrationAndSyncModule(dmiRegistration)
+ // Spock validated invocation order between multiple then blocks
+ then: 'cm-handles are removed first'
+ 1 * objectUnderTest.parseAndRemoveCmHandlesInDmiRegistration(*_)
+ then: 'cm-handles are created'
+ 1 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(*_)
+ then: 'cm-handles are updated'
+ 1 * mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_)
}
- def 'Register a DMI Plugin for the given cm-handle(s) without DMI properties.'() {
- given: 'a registration without cm-handle properties'
- NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
- def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:'my-server')
- ncmpServiceCmHandle.cmHandleID = '123'
- ncmpServiceCmHandle.dmiProperties = Collections.emptyMap()
- ncmpServiceCmHandle.publicProperties = Collections.emptyMap()
- dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
- def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","dmi-data-service-name":null,"dmi-model-service-name":null,"additional-properties":[],"public-properties":[]}]}'
- when: 'registration is updated'
- objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
- then: 'save list elements is invoked with the expected parameters'
- 1 * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
- '/dmi-registry', expectedJsonData, noTimestamp)
- }
+ def 'DMI Registration: Response from all operations types are in response'() {
+ given: 'a registration with operations of all three types'
+ def dmiRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
+ dmiRegistration.setCreatedCmHandles([new NcmpServiceCmHandle(cmHandleId: 'cmhandle-1', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
+ dmiRegistration.setUpdatedCmHandles([new NcmpServiceCmHandle(cmHandleId: 'cmhandle-2', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
+ dmiRegistration.setRemovedCmHandles(['cmhandle-2'])
+ and: 'update cm-handles can be processed successfully'
+ def updateResponses = [CmHandleRegistrationResponse.createSuccessResponse('cmhandle-2')]
+ mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_) >> updateResponses
+ and: 'create cm-handles can be processed successfully'
+ def createdResponses = [CmHandleRegistrationResponse.createSuccessResponse('cmhandle-1')]
+ objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(*_) >> createdResponses
+ and: 'delete cm-handles can be processed successfully'
+ def removeResponses = [CmHandleRegistrationResponse.createSuccessResponse('cmhandle-3')]
+ objectUnderTest.parseAndRemoveCmHandlesInDmiRegistration(*_) >> removeResponses
+ when: 'registration is processed'
+ def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiRegistration)
+ then: 'response has values from all operations'
+ response.getRemovedCmHandles() == removeResponses
+ response.getCreatedCmHandles() == createdResponses
+ response.getUpdatedCmHandles() == updateResponses
- def 'Register a DMI Plugin for a given cm-handle(s) with JSON processing errors during process.'() {
- given: 'a registration without cm-handle properties '
- NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
- def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:'some-plugin')
- dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
- and: 'an json processing exception occurs'
- spiedJsonObjectMapper.asJsonString(_) >> { throw (new JsonProcessingException('')) }
- when: 'registration is updated and modules are synced'
- objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
- then: 'a data validation exception is thrown'
- thrown(DataValidationException)
- }
- def 'Register a DMI Plugin for the given cm-handle(s) with no data found during delete process.'() {
- given: 'a registration without cm-handle properties '
- NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
- def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:'some-plugin')
- dmiPluginRegistration.removedCmHandles = ['some cm handle']
- and: 'an json processing exception occurs during delete process'
- mockCpsDataService.deleteListOrListElement(*_) >> { throw (new DataNodeNotFoundException('','')) }
- when: 'registration is updated and modules are synced'
- objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
- then: 'no exception is thrown'
- noExceptionThrown()
}
- def 'Register a DMI Plugin for the given cm-handle(s) with no schema set found during delete process.'() {
- given: 'a registration'
- def objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
- def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:'my-server')
- dmiPluginRegistration.removedCmHandles = cmHandlesArray
- and: 'an exception occurs during delete schema set process'
- mockCpsModuleService.deleteSchemaSet(_,_,_) >> { throw (new Exception('')) }
- when: 'registration is updated and modules are synced'
- objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
- then: 'delete list or list element is still called'
- 1 * mockCpsDataService.deleteListOrListElement(_,_,_,_)
- }
-
- def 'Dmi plugin registration with #scenario'() {
+ def 'Create CM-handle Validation: Registration with valid Service names: #scenario'() {
given: 'a registration '
- def objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
- def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:dmiPlugin, dmiModelPlugin:dmiModelPlugin,
- dmiDataPlugin:dmiDataPlugin)
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
+ dmiDataPlugin: dmiDataPlugin)
dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
when: 'update registration and sync module is called with correct DMI plugin information'
objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
@@ -165,11 +128,10 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
'data & model using same service' | '' | 'service1' | 'service1'
}
- def 'Invalid DMI plugin registration with #scenario'() {
+ def 'Create CM-handle Validation: Invalid DMI plugin service name with #scenario'() {
given: 'a registration '
- def objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
- def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:dmiPlugin, dmiModelPlugin:dmiModelPlugin,
- dmiDataPlugin:dmiDataPlugin)
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
+ dmiDataPlugin: dmiDataPlugin)
dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
when: 'registration is called with incorrect DMI plugin information'
objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
@@ -179,37 +141,254 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
and: 'registration is not called'
0 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration)
where:
- scenario | dmiPlugin | dmiModelPlugin | dmiDataPlugin || expectedMessageDetails
- 'empty DMI plugins' | '' | '' | '' || 'No DMI plugin service names'
- 'blank DMI plugins' | ' ' | ' ' | ' ' || 'No DMI plugin service names'
- 'null DMI plugins' | null | null | null || 'No DMI plugin service names'
- 'all DMI plugins' | 'service1' | 'service2' | 'service3' || 'Cannot register combined plugin service name and other service names'
- '(combined)DMI and Data Plugin' | 'service1' | '' | 'service2' || 'Cannot register combined plugin service name and other service names'
- '(combined)DMI and model Plugin'| 'service1' | 'service2' | '' || 'Cannot register combined plugin service name and other service names'
- 'only model DMI plugin' | '' | 'service1' | '' || 'Cannot register just a Data or Model plugin service name'
- 'only data DMI plugin' | '' | '' | 'service1' || 'Cannot register just a Data or Model plugin service name'
+ scenario | dmiPlugin | dmiModelPlugin | dmiDataPlugin || expectedMessageDetails
+ 'empty DMI plugins' | '' | '' | '' || 'No DMI plugin service names'
+ 'blank DMI plugins' | ' ' | ' ' | ' ' || 'No DMI plugin service names'
+ 'null DMI plugins' | null | null | null || 'No DMI plugin service names'
+ 'all DMI plugins' | 'service1' | 'service2' | 'service3' || 'Cannot register combined plugin service name and other service names'
+ '(combined)DMI and Data Plugin' | 'service1' | '' | 'service2' || 'Cannot register combined plugin service name and other service names'
+ '(combined)DMI and model Plugin' | 'service1' | 'service2' | '' || 'Cannot register combined plugin service name and other service names'
+ 'only model DMI plugin' | '' | 'service1' | '' || 'Cannot register just a Data or Model plugin service name'
+ 'only data DMI plugin' | '' | '' | 'service1' || 'Cannot register just a Data or Model plugin service name'
+ }
+
+ def 'Create CM-Handle Successfully: #scenario.'() {
+ given: 'a registration without cm-handle properties'
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
+ dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleId: 'cmhandle', dmiProperties: dmiProperties, publicProperties: publicProperties)]
+ when: 'registration is updated'
+ def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+ then: 'a successful response is received'
+ response.getCreatedCmHandles().size() == 1
+ with(response.getCreatedCmHandles().get(0)) {
+ assert it.status == Status.SUCCESS
+ assert it.cmHandle == 'cmhandle'
+ }
+ and: 'save list elements is invoked with the expected parameters'
+ interaction {
+ def expectedJsonData = """{"cm-handles":[{"id":"cmhandle","dmi-service-name":"my-server","additional-properties":$expectedDmiProperties,"public-properties":$expectedPublicProperties}]}"""
+ 1 * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
+ '/dmi-registry', expectedJsonData, noTimestamp)
+ }
+ then: 'model sync is invoked with expected parameters'
+ 1 * objectUnderTest.syncModulesAndCreateAnchor(_) >> { YangModelCmHandle yangModelCmHandle ->
+ {
+ assert yangModelCmHandle.id == 'cmhandle'
+ assert yangModelCmHandle.dmiServiceName == 'my-server'
+ assert spiedJsonObjectMapper.asJsonString(yangModelCmHandle.getPublicProperties()) == expectedPublicProperties
+ assert spiedJsonObjectMapper.asJsonString(yangModelCmHandle.getDmiProperties()) == expectedDmiProperties
+
+ }
+ }
+ where:
+ scenario | dmiProperties | publicProperties || expectedDmiProperties | expectedPublicProperties
+ 'with dmi & public properties' | ['dmi-key': 'dmi-value'] | ['public-key': 'public-value'] || '[{"name":"dmi-key","value":"dmi-value"}]' | '[{"name":"public-key","value":"public-value"}]'
+ 'with only public properties' | [:] | ['public-key': 'public-value'] || '[]' | '[{"name":"public-key","value":"public-value"}]'
+ 'with only dmi properties' | ['dmi-key': 'dmi-value'] | [:] || '[{"name":"dmi-key","value":"dmi-value"}]' | '[]'
+ 'without dmi & public properties' | [:] | [:] || '[]' | '[]'
+
}
- def 'Exception thrown on CM-Handle registration update request'() {
- given: 'a CM-handle registration'
- def objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
- and: 'dmi plugin registration input update request'
- def dmiPluginReg = new DmiPluginRegistration();
- dmiPluginReg.dmiPlugin = 'onap.dmap.plugin';
- dmiPluginReg.updatedCmHandles = [new NcmpServiceCmHandle(cmHandleID: 'unknownHandle')]
- and: 'update data node leaves is unable to find data node'
- mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_) >> { throw new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry') }
- when: 'update dmi registration is called'
- objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginReg)
- then: 'data validation exception is thrown'
- def exceptionThrown = thrown(DataValidationException.class)
- assert exceptionThrown.getDetails().contains('DataNode not found')
+ def 'Create CM-Handle Multiple Requests: All cm-handles creation requests are processed'() {
+ given: 'a registration with three cm-handles to be created'
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
+ createdCmHandles: [new NcmpServiceCmHandle(cmHandleId: 'cmhandle1'),
+ new NcmpServiceCmHandle(cmHandleId: 'cmhandle2'),
+ new NcmpServiceCmHandle(cmHandleId: 'cmhandle3')])
+ and: 'cm-handle creation is successful for 1st and 3rd; failed for 2nd'
+ mockCpsDataService.saveListElements(_, _, _, _, _) >> {} >> { throw new RuntimeException("Failed") } >> {}
+ when: 'registration is updated to create cm-handles'
+ def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+ then: 'a response is received for all cm-handles'
+ response.getCreatedCmHandles().size() == 3
+ and: '1st and 3rd cm-handle are created successfully'
+ with(response.getCreatedCmHandles().get(0)) {
+ assert it.status == Status.SUCCESS
+ assert it.cmHandle == 'cmhandle1'
+ }
+ with(response.getCreatedCmHandles().get(2)) {
+ assert it.status == Status.SUCCESS
+ assert it.cmHandle == 'cmhandle3'
+ }
+ and: '2nd cm-handle creation fails'
+ with(response.getCreatedCmHandles().get(1)) {
+ assert it.status == Status.FAILURE
+ assert it.registrationError == UNKNOWN_ERROR
+ assert it.errorText == 'Failed'
+ assert it.cmHandle == 'cmhandle2'
+ }
+ }
+
+ def 'Create CM-Handle Error Handling: Registration fails: #scenario'() {
+ given: 'a registration without cm-handle properties'
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
+ dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleId: cmHandleId)]
+ and: 'cm-handler registration fails: #scenario'
+ mockCpsDataService.saveListElements(_, _, _, _, _) >> { throw exception }
+ when: 'registration is updated'
+ def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+ then: 'a failure response is received'
+ response.getCreatedCmHandles().size() == 1
+ with(response.getCreatedCmHandles().get(0)) {
+ assert it.status == Status.FAILURE
+ assert it.cmHandle == cmHandleId
+ assert it.registrationError == expectedError
+ assert it.errorText == expectedErrorText
+ }
+ and: 'model-sync is not invoked'
+ 0 * objectUnderTest.syncModulesAndCreateAnchor(_)
+ where:
+ scenario | cmHandleId | exception || expectedError | expectedErrorText
+ 'cm-handle already exist' | 'cmhandle' | new AlreadyDefinedException('', new RuntimeException()) || CM_HANDLE_ALREADY_EXIST | 'cm-handle already exists'
+ 'cm-handle has invalid name' | 'cm handle with space' | new DataValidationException("", "") || CM_HANDLE_INVALID_ID | 'cm-handle has an invalid character(s) in id'
+ 'unknown exception while registering cm-handle' | 'cmhandle' | new RuntimeException('Failed') || UNKNOWN_ERROR | 'Failed'
+ }
+
+ def 'Create CM-Handle Error Handling: Model Sync fails'() {
+ given: 'objects under test without disabled model sync'
+ def objectUnderTest = getObjectUnderTest()
+ and: 'a registration without cm-handle properties'
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
+ dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleId: 'cmhandle')]
+ and: 'cm-handler models sync fails'
+ objectUnderTest.syncModulesAndCreateAnchor(*_) >> { throw new RuntimeException('Model-Sync failed') }
+ when: 'registration is updated'
+ def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+ then: 'a failure response is received'
+ response.getCreatedCmHandles().size() == 1
+ with(response.getCreatedCmHandles().get(0)) {
+ assert it.status == Status.FAILURE
+ assert it.cmHandle == 'cmhandle'
+ assert it.registrationError == UNKNOWN_ERROR
+ assert it.errorText == 'Model-Sync failed'
+ }
+ and: 'cm-handle is registered'
+ 1 * mockCpsDataService.saveListElements(*_)
+ }
+
+ def 'Update CM-Handle: Update Operation Response is added to the response'() {
+ given: 'a registration to update CmHandles'
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
+ updatedCmHandles: [{}])
+ and: 'cm-handle updates can be processed successfully'
+ def updateOperationResponse = [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-1'),
+ CmHandleRegistrationResponse.createFailureResponse('cm-handle-2', new Exception("Failed")),
+ CmHandleRegistrationResponse.createFailureResponse('cm-handle-3', CM_HANDLE_DOES_NOT_EXIST),
+ CmHandleRegistrationResponse.createFailureResponse('cm handle 4', CM_HANDLE_INVALID_ID)]
+ mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(_) >> updateOperationResponse
+ when: 'registration is updated'
+ def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+ then: 'the response contains updateOperationResponse'
+ assert response.getUpdatedCmHandles().size() == 4
+ assert response.getUpdatedCmHandles().containsAll(updateOperationResponse)
+ }
+
+ def 'Remove CmHandle Successfully: #scenario'() {
+ given: 'a registration'
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
+ removedCmHandles: ['cmhandle'])
+ and: '#scenario'
+ mockCpsModuleService.deleteSchemaSet(_, 'cmhandle', CASCADE_DELETE_ALLOWED) >>
+ { if (!schemaSetExist) { throw new SchemaSetNotFoundException("", "") } }
+ when: 'registration is updated to delete cmhandle'
+ def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+ then: 'delete list or list element is called'
+ 1 * mockCpsDataService.deleteListOrListElement(_, _, _, _)
+ and: 'successful response is received'
+ assert response.getRemovedCmHandles().size() == 1
+ with(response.getRemovedCmHandles().get(0)) {
+ assert it.status == Status.SUCCESS
+ assert it.cmHandle == 'cmhandle'
+ }
+ where:
+ scenario | schemaSetExist
+ 'schema-set exists and can be deleted successfully' | true
+ 'schema-set does not exist' | false
+ }
+
+ def 'Remove CmHandle: All cm-handles delete requests are processed'() {
+ given: 'a registration with three cm-handles to be deleted'
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
+ removedCmHandles: ['cmhandle1', 'cmhandle2', 'cmhandle3'])
+ and: 'cm-handle deletion is successful for 1st and 3rd; failed for 2nd'
+ mockCpsDataService.deleteListOrListElement(_, _, _, _) >> {} >> { throw new RuntimeException("Failed") } >> {}
+ when: 'registration is updated to delete cmhandles'
+ def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+ then: 'a response is received for all cm-handles'
+ response.getRemovedCmHandles().size() == 3
+ and: '1st and 3rd cm-handle deletes successfully'
+ with(response.getRemovedCmHandles().get(0)) {
+ assert it.status == Status.SUCCESS
+ assert it.cmHandle == 'cmhandle1'
+ }
+ with(response.getRemovedCmHandles().get(2)) {
+ assert it.status == Status.SUCCESS
+ assert it.cmHandle == 'cmhandle3'
+ }
+ and: '2nd cm-handle deletion fails'
+ with(response.getRemovedCmHandles().get(1)) {
+ assert it.status == Status.FAILURE
+ assert it.registrationError == UNKNOWN_ERROR
+ assert it.errorText == 'Failed'
+ assert it.cmHandle == 'cmhandle2'
+ }
+ }
+
+ def 'Remove CmHandle Error Handling: Schema Set Deletion failed'() {
+ given: 'a registration'
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
+ removedCmHandles: ['cmhandle'])
+ and: 'schema set deletion failed with unknown error'
+ mockCpsModuleService.deleteSchemaSet(_, _, _) >> { throw new RuntimeException('Failed') }
+ when: 'registration is updated to delete cmhandle'
+ def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+ then: 'no exception is thrown'
+ noExceptionThrown()
+ and: 'cm-handle is not deleted'
+ 0 * mockCpsDataService.deleteListOrListElement(_, _, _, _)
+ and: 'a failure response is received'
+ assert response.getRemovedCmHandles().size() == 1
+ with(response.getRemovedCmHandles().get(0)) {
+ assert it.status == Status.FAILURE
+ assert it.cmHandle == 'cmhandle'
+ assert it.errorText == 'Failed'
+ assert it.registrationError == UNKNOWN_ERROR
+ }
+ }
+
+ def 'Remove CmHandle Error Handling: #scenario'() {
+ given: 'a registration'
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
+ removedCmHandles: ['cmhandle'])
+ and: 'cm-handle deletion throws exception'
+ mockCpsDataService.deleteListOrListElement(_, _, _, _) >> { throw deleteListElementException }
+ when: 'registration is updated to delete cmhandle'
+ def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+ then: 'no exception is thrown'
+ noExceptionThrown()
+ and: 'a failure response is received'
+ assert response.getRemovedCmHandles().size() == 1
+ with(response.getRemovedCmHandles().get(0)) {
+ assert it.status == Status.FAILURE
+ assert it.cmHandle == 'cmhandle'
+ assert it.registrationError == expectedError
+ assert it.errorText == expectedErrorText
+ }
+ where:
+ scenario | cmHandleId | deleteListElementException || expectedError | expectedErrorText
+ 'cm-handle does not exist' | 'cmhandle' | new DataNodeNotFoundException("", "", "") || CM_HANDLE_DOES_NOT_EXIST | 'cm-handle does not exist'
+ 'cm-handle has invalid name' | 'cm handle with space' | new DataValidationException("", "") || CM_HANDLE_INVALID_ID | 'cm-handle has an invalid character(s) in id'
+ 'an unexpected exception' | 'cmhandle' | new RuntimeException("Failed") || UNKNOWN_ERROR | 'Failed'
}
def getObjectUnderTestWithModelSyncDisabled() {
- def objectUnderTest = Spy(new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations,
- mockCpsModuleService, mockCpsAdminService, mockNetworkCmProxyDataServicePropertyHandler,mockYangModelCmHandleRetriever))
+ def objectUnderTest = getObjectUnderTest()
objectUnderTest.syncModulesAndCreateAnchor(*_) >> null
return objectUnderTest
}
+
+ def getObjectUnderTest() {
+ return Spy(new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations,
+ mockCpsModuleService, mockCpsAdminService, mockNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever, mockModuleSyncService))
+ }
}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
index c21d7e774..7629500db 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
@@ -2,7 +2,7 @@
* ============LICENSE_START=======================================================
* Copyright (C) 2021-2022 Nordix Foundation
* Modifications Copyright (C) 2021 Pantheon.tech
- * Modifications Copyright (C) 2021 Bell Canada
+ * Modifications Copyright (C) 2021-2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,9 +22,13 @@
package org.onap.cps.ncmp.api.impl
-import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException
+import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException
import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever
import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
+import org.onap.cps.ncmp.api.models.DmiPluginRegistration
+import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
+import org.onap.cps.spi.exceptions.DataValidationException
+import org.onap.cps.ncmp.api.inventory.sync.ModuleSyncService
import spock.lang.Shared
import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL
@@ -33,14 +37,12 @@ import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum
import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.READ
import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.UPDATE
-import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations
import org.onap.cps.utils.JsonObjectMapper
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import org.onap.cps.api.CpsAdminService
import org.onap.cps.api.CpsDataService
import org.onap.cps.api.CpsModuleService
-import org.onap.cps.ncmp.api.impl.exception.ServerNcmpException
import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
import org.onap.cps.spi.FetchDescendantsOption
import org.onap.cps.spi.model.DataNode
@@ -54,17 +56,21 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
def mockCpsModuleService = Mock(CpsModuleService)
def mockCpsAdminService = Mock(CpsAdminService)
def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
- def mockDmiModelOperations = Mock(DmiModelOperations)
def mockDmiDataOperations = Mock(DmiDataOperations)
def nullNetworkCmProxyDataServicePropertyHandler = null
def mockYangModelCmHandleRetriever = Mock(YangModelCmHandleRetriever)
+ def mockModuleSyncService = Mock(ModuleSyncService)
+ def mockDmiPluginRegistration = Mock(DmiPluginRegistration)
+
def NO_TOPIC = null
def NO_REQUEST_ID = null
@Shared
def OPTIONS_PARAM = '(a=1,b=2)'
+ @Shared
+ def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'some-cm-handle-id')
- def objectUnderTest = new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations,
- mockCpsModuleService, mockCpsAdminService, nullNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever)
+ def objectUnderTest = new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations,
+ mockCpsModuleService, mockCpsAdminService, nullNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever, mockModuleSyncService)
def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
@@ -84,6 +90,17 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
>> { new ResponseEntity<>(HttpStatus.CREATED) }
}
+ def 'Write resource data for pass-through running from DMI using an invalid id.'() {
+ when: 'write resource data is called'
+ objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('invalid cm handle name',
+ 'testResourceId', CREATE,
+ '{some-json}', 'application/json')
+ then: 'exception is thrown'
+ thrown(DataValidationException.class)
+ and: 'DMI is not invoked'
+ 0 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi(_, _, _, _, _)
+ }
+
def 'Write resource data for pass-through running from DMI using POST "not found" response (from DMI).'() {
given: 'cpsDataService returns valid dataNode'
mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
@@ -98,9 +115,9 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
'testResourceId', CREATE,
'{some-json}', 'application/json')
then: 'exception is thrown'
- def exceptionThrown = thrown(ServerNcmpException.class)
- and: 'details contains (not found) error code: 404'
- exceptionThrown.details.contains('404')
+ def exceptionThrown = thrown(HttpClientRequestException.class)
+ and: 'http status (not found) error code: 404'
+ exceptionThrown.httpStatus == HttpStatus.NOT_FOUND.value()
}
def 'Get resource data for pass-through operational from DMI.'() {
@@ -112,20 +129,30 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
'testCmHandle',
'testResourceId',
OPTIONS_PARAM,
- 'testAcceptParam',
PASSTHROUGH_OPERATIONAL,
NO_REQUEST_ID,
NO_TOPIC) >> new ResponseEntity<>('dmi-response', HttpStatus.OK)
when: 'get resource data operational for cm-handle is called'
def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
'testResourceId',
- 'testAcceptParam',
OPTIONS_PARAM,
- NO_TOPIC)
+ NO_TOPIC,
+ NO_REQUEST_ID)
then: 'DMI returns a json response'
response == 'dmi-response'
}
+ def 'Get resource data for pass-through operational from DMI with invalid name.'() {\
+ when: 'get resource data operational for cm-handle is called'
+ objectUnderTest.getResourceDataOperationalForCmHandle('invalid test cm handle',
+ 'testResourceId',
+ OPTIONS_PARAM,
+ NO_TOPIC,
+ NO_REQUEST_ID)
+ then: 'A data validation Exception is thrown'
+ thrown(DataValidationException)
+ }
+
def 'Get resource data for pass-through operational from DMI with Json Processing Exception.'() {
given: 'cps data service returns valid data node'
mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
@@ -138,12 +165,13 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
when: 'get resource data is called'
objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
'testResourceId',
- 'testAcceptParam',
OPTIONS_PARAM,
- NO_TOPIC)
- then: 'exception is thrown with the expected details'
- def exceptionThrown = thrown(ServerNcmpException.class)
- exceptionThrown.details == 'DMI status code: 404, DMI response body: NOK-json'
+ NO_TOPIC,
+ NO_REQUEST_ID)
+ then: 'exception is thrown with the expected response code and details'
+ def exceptionThrown = thrown(HttpClientRequestException.class)
+ exceptionThrown.details.contains('NOK-json')
+ exceptionThrown.httpStatus == HttpStatus.NOT_FOUND.value()
}
def 'Get resource data for pass-through operational from DMI return NOK response.'() {
@@ -154,7 +182,6 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
'testResourceId',
OPTIONS_PARAM,
- 'testAcceptParam',
PASSTHROUGH_OPERATIONAL,
NO_REQUEST_ID,
NO_TOPIC)
@@ -162,12 +189,13 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
when: 'get resource data is called'
objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
'testResourceId',
- 'testAcceptParam',
OPTIONS_PARAM,
- NO_TOPIC)
+ NO_TOPIC,
+ NO_REQUEST_ID)
then: 'exception is thrown'
- def exceptionThrown = thrown(ServerNcmpException.class)
- and: 'details contains the original response'
+ def exceptionThrown = thrown(HttpClientRequestException.class)
+ and: 'details contain the original response'
+ exceptionThrown.httpStatus == HttpStatus.NOT_FOUND.value()
exceptionThrown.details.contains('NOK-json')
}
@@ -179,20 +207,30 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
'testResourceId',
OPTIONS_PARAM,
- 'testAcceptParam',
PASSTHROUGH_RUNNING,
NO_REQUEST_ID,
NO_TOPIC) >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
when: 'get resource data is called'
def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
'testResourceId',
- 'testAcceptParam',
OPTIONS_PARAM,
- NO_TOPIC)
+ NO_TOPIC,
+ NO_REQUEST_ID)
then: 'get resource data returns expected response'
response == '{dmi-response}'
}
+ def 'Get resource data for pass-through running from DMI with invalid name.'() {
+ when: 'get resource data operational for cm-handle is called'
+ objectUnderTest.getResourceDataPassThroughRunningForCmHandle('invalid test cm handle',
+ 'testResourceId',
+ OPTIONS_PARAM,
+ NO_TOPIC,
+ NO_REQUEST_ID)
+ then: 'A data validation Exception is thrown'
+ thrown(DataValidationException)
+ }
+
def 'Get resource data for pass-through running from DMI return NOK response.'() {
given: 'cpsDataService returns valid dataNode'
mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
@@ -201,7 +239,6 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
'testResourceId',
OPTIONS_PARAM,
- 'testAcceptParam',
PASSTHROUGH_RUNNING,
NO_REQUEST_ID,
NO_TOPIC)
@@ -209,86 +246,30 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
when: 'get resource data is called'
objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
'testResourceId',
- 'testAcceptParam',
OPTIONS_PARAM,
- NO_TOPIC)
+ NO_TOPIC,
+ NO_REQUEST_ID)
then: 'exception is thrown'
- def exceptionThrown = thrown(ServerNcmpException.class)
- and: 'details contains the original response'
+ def exceptionThrown = thrown(HttpClientRequestException.class)
+ and: 'details contain the original response'
exceptionThrown.details.contains('NOK-json')
- }
-
- def 'DMI Operational data request with #scenario'() {
- given: 'cps data service returns valid data node'
- mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
- cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
- and: 'dmi data operation returns valid response and data'
- mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, NO_REQUEST_ID, NO_TOPIC)
- >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
- when: 'get resource data is called data operational with blank topic'
- def responseData = objectUnderTest.getResourceDataOperationalForCmHandle('', '',
- '', '', emptyTopic)
- then: 'a invalid topic exception is thrown'
- thrown(InvalidTopicException)
- where: 'the following parameters are used'
- scenario | emptyTopic
- 'no topic value in url' | ''
- 'empty topic value in url' | '\"\"'
- 'blank topic value in url' | ' '
- 'invalid non-empty topic value in url' | '1_5_*_#'
- }
-
- def 'Get resource data for data operational from DMI with valid topic i.e. async request.'() {
- given: 'cps data service returns valid data node'
- mockCpsDataService.getDataNode(*_) >> dataNode
- and: 'dmi data operation returns valid response and data'
- mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, _, 'my-topic-name')
- >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
- when: 'get resource data is called for data operational with valid topic'
- def responseData = objectUnderTest.getResourceDataOperationalForCmHandle('', '', '', '', 'my-topic-name')
- then: 'non empty request id is generated'
- assert responseData.body.requestId.length() > 0
- }
-
- def 'Get resource data for pass through running from DMI with valid topic async request.'() {
- given: 'cps data service returns valid data node'
- mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
- cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
- and: 'dmi data operation returns valid response and data'
- mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, _, 'my-topic-name')
- >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
- when: 'get resource data is called for data operational with valid topic'
- def responseData = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('',
- '', '', OPTIONS_PARAM, 'my-topic-name')
- then: 'non empty request id is generated'
- assert responseData.body.requestId.length() > 0
- }
-
- def 'DMI pass through running data request with #scenario'() {
- given: 'cps data service returns valid data node'
- mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
- cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
- and: 'dmi data operation returns valid response and data'
- mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, NO_REQUEST_ID, NO_TOPIC)
- >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
- when: 'get resource data is called for data operational with valid topic'
- def responseData = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('',
- '', '', '', emptyTopic)
- then: 'a invalid topic exception is thrown'
- thrown(InvalidTopicException)
- where: 'the following parameters are used'
- scenario | emptyTopic
- 'no topic value in url' | ''
- 'empty topic value in url' | '\"\"'
- 'blank topic value in url' | ' '
- 'invalid non-empty topic value in url' | '1_5_*_#'
+ exceptionThrown.httpStatus == HttpStatus.NOT_FOUND.value()
}
def 'Getting Yang Resources.'() {
when: 'yang resources is called'
- objectUnderTest.getYangResourcesModuleReferences('some cm handle')
+ objectUnderTest.getYangResourcesModuleReferences('some-cm-handle')
then: 'CPS module services is invoked for the correct dataspace and cm handle'
- 1 * mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some cm handle')
+ 1 * mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cm-handle')
+ }
+
+ def 'Getting Yang Resources with an invalid #scenario.'() {
+ when: 'yang resources is called'
+ objectUnderTest.getYangResourcesModuleReferences('invalid cm handle with spaces')
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'CPS module services is not invoked'
+ 0 * mockCpsModuleService.getYangResourcesModuleReferences(_, _)
}
def 'Get cm handle identifiers for the given module names.'() {
@@ -308,12 +289,21 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
when: 'getting cm handle details for a given cm handle id from ncmp service'
def result = objectUnderTest.getNcmpServiceCmHandle('Some-Cm-Handle')
then: 'the result returns the correct data'
- result.cmHandleID == 'Some-Cm-Handle'
+ result.cmHandleId == 'Some-Cm-Handle'
result.dmiProperties ==[ Book:'Romance Novel' ]
result.publicProperties == [ "Public Book":'Public Romance Novel' ]
}
+ def 'Get a cm handle with an invalid id.'() {
+ when: 'getting cm handle details for a given cm handle id with an invalid name'
+ objectUnderTest.getNcmpServiceCmHandle('invalid cm handle with spaces')
+ then: 'an exception is thrown'
+ thrown(DataValidationException)
+ and: 'the yang model cm handle retriever is not invoked'
+ 0 * mockYangModelCmHandleRetriever.getDmiServiceNamesAndProperties(_)
+ }
+
def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() {
given: 'cpsDataService returns valid datanode'
mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
@@ -340,12 +330,28 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
'{some-json}',
'application/json')
then: 'an exception is thrown with the expected error message details with correct operation'
- def exceptionThrown = thrown(ServerNcmpException.class)
+ def exceptionThrown = thrown(HttpClientRequestException.class)
exceptionThrown.getMessage().contains(expectedResponseMessage)
where:
scenario | givenOperation || expectedResponseMessage
- 'CREATE' | CREATE || 'Not able to create resource data.'
- 'READ' | READ || 'Not able to read resource data.'
- 'UPDATE' | UPDATE || 'Not able to update resource data.'
+ 'CREATE' | CREATE || 'Unable to create resource data.'
+ 'READ' | READ || 'Unable to read resource data.'
+ 'UPDATE' | UPDATE || 'Unable to update resource data.'
+ }
+
+ def 'Verify modules and create anchor params'() {
+ given: 'dmi plugin registration return created cm handles'
+ def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'service1', dmiModelPlugin: 'service1',
+ dmiDataPlugin: 'service2')
+ dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
+ mockDmiPluginRegistration.getCreatedCmHandles() >> [ncmpServiceCmHandle]
+ when: 'parse and create cm handle in dmi registration then sync module'
+ objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(mockDmiPluginRegistration)
+ then: 'validate params for creating anchor and list elements'
+ 1 * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
+ '/dmi-registry', '{"cm-handles":[{"id":"some-cm-handle-id",' +
+ '"additional-properties":[],"public-properties":[]}]}', null)
+ 1 * mockCpsAdminService.createAnchor('NFP-Operational', null,
+ 'some-cm-handle-id')
}
}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy
index 9b8d4ada5..5eba5eecd 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2022 Nordix Foundation
+ * Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,11 +21,17 @@
package org.onap.cps.ncmp.api.impl
+import org.onap.cps.spi.exceptions.DataValidationException
+
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_DOES_NOT_EXIST
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_INVALID_ID
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.UNKNOWN_ERROR
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status
+
import org.onap.cps.api.CpsDataService
import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
import org.onap.cps.spi.FetchDescendantsOption
import org.onap.cps.spi.exceptions.DataNodeNotFoundException
-import org.onap.cps.spi.exceptions.DataValidationException
import org.onap.cps.spi.model.DataNode
import org.onap.cps.spi.model.DataNodeBuilder
import spock.lang.Specification
@@ -50,7 +57,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
given: 'the CPS service return a CM handle'
mockCpsDataService.getDataNode(dataspaceName, anchorName, cmHandleXpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
and: 'an update cm handle request with public properties updates'
- def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleID: cmHandleId, publicProperties: updatedPublicProperties)]
+ def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: updatedPublicProperties)]
when: 'update data node leaves is called with the update request'
objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest)
then: 'the replace list method is called with correct params'
@@ -72,7 +79,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
given: 'the CPS service return a CM handle'
mockCpsDataService.getDataNode(dataspaceName, anchorName, cmHandleXpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
and: 'an update cm handle request with DMI properties updates'
- def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleID: cmHandleId, dmiProperties: updatedDmiProperties)]
+ def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: updatedDmiProperties)]
when: 'update data node leaves is called with the update request'
objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest)
then: 'replace list method should is called with correct params'
@@ -96,7 +103,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
def cmHandleDataNode = new DataNode(xpath: cmHandleXpath, childDataNodes: originalPropertyDataNodes)
mockCpsDataService.getDataNode(dataspaceName, anchorName, cmHandleXpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
and: 'an update cm handle request that removes all public properties(existing and non-existing)'
- def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleID: cmHandleId, publicProperties: ['publicProp3': null, 'publicProp4': null])]
+ def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp3': null, 'publicProp4': null])]
when: 'update data node leaves is called with the update request'
objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest)
then: 'the replace list method is not called'
@@ -113,16 +120,58 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
'no original properties' | [] || 0
}
- def 'Exception thrown when we try to update cmHandle'() {
+ def '#scenario error leads to #exception when we try to update cmHandle'() {
given: 'cm handles request'
- def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleID: cmHandleId, publicProperties: [:], dmiProperties: [:])]
+ def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: [:], dmiProperties: [:])]
and: 'data node cannot be found'
- mockCpsDataService.getDataNode(*_) >> { throw new DataNodeNotFoundException(dataspaceName, anchorName, cmHandleXpath) }
+ mockCpsDataService.getDataNode(*_) >> { throw exception }
when: 'update data node leaves is called using correct parameters'
- objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest)
- then: 'data validation exception is thrown'
- def exceptionThrown = thrown(DataValidationException.class)
- assert exceptionThrown.getMessage().contains('DataNode not found')
+ def response = objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest)
+ then: 'one failed registration response'
+ response.size() == 1
+ and: 'it has expected error details'
+ with(response.get(0)) {
+ assert it.status == Status.FAILURE
+ assert it.cmHandle == cmHandleId
+ assert it.registrationError == expectedError
+ assert it.errorText == expectedErrorText
+ }
+ where:
+ scenario | cmHandleId | exception || expectedError | expectedErrorText
+ 'Cm Handle does not exist' | 'cmHandleId' | new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry') || CM_HANDLE_DOES_NOT_EXIST | 'cm-handle does not exist'
+ 'Unknown' | 'cmHandleId' | new RuntimeException('Failed') || UNKNOWN_ERROR | 'Failed'
+ 'Invalid cm handle id' | 'cmHandleId with spaces' | new DataValidationException('Name Validation Error.', cmHandleId + 'contains an invalid character') || CM_HANDLE_INVALID_ID | 'cm-handle has an invalid character(s) in id'
+ }
+
+ def 'Multiple update operations in a single request'() {
+ given: 'cm handles request'
+ def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:]),
+ new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:]),
+ new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:])]
+ and: 'data node can be found for 1st and 3rd cm-handle but not for 2nd cm-handle'
+ mockCpsDataService.getDataNode(*_) >> cmHandleDataNode >> { throw new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry') } >> cmHandleDataNode
+ when: 'update data node leaves is called using correct parameters'
+ def cmHandleResponseList = objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest)
+ then: 'response has 3 values'
+ cmHandleResponseList.size() == 3
+ and: 'the 1st and 3rd requests were processed successfully'
+ with(cmHandleResponseList.get(0)) {
+ assert it.status == Status.SUCCESS
+ assert it.cmHandle == cmHandleId
+ }
+ with(cmHandleResponseList.get(2)) {
+ assert it.status == Status.SUCCESS
+ assert it.cmHandle == cmHandleId
+ }
+ and: 'the 2nd request failed with correct error code'
+ with(cmHandleResponseList.get(1)) {
+ assert it.status == Status.FAILURE
+ assert it.cmHandle == cmHandleId
+ assert it.registrationError == CM_HANDLE_DOES_NOT_EXIST
+ assert it.errorText == "cm-handle does not exist"
+ }
+ then: 'the replace list method is called twice'
+ 2 * mockCpsDataService.replaceListContent(*_)
}
def convertToProperties(expectedPropertiesAfterUpdateAsMap) {
@@ -133,4 +182,5 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
}))
return properties
}
+
}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy
index 389086c77..394df1d07 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2021 Nordix Foundation
+ * Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -43,22 +44,12 @@ class DmiRestClientSpec extends Specification {
DmiRestClient objectUnderTest
def resourceUrl = 'some url'
- def 'DMI POST operation'() {
- given: 'the rest template returns a valid response entity'
- def mockResponseEntity = Mock(ResponseEntity)
- mockRestTemplate.exchange(resourceUrl, HttpMethod.POST, _ as HttpEntity, Object.class) >> mockResponseEntity
- when: 'POST operation is invoked'
- def result = objectUnderTest.postOperation(resourceUrl, new HttpHeaders())
- then: 'the output of the method is equal to the output from the rest template'
- result == mockResponseEntity
- }
-
def 'DMI POST operation with JSON.'() {
given: 'the rest template returns a valid response entity'
def mockResponseEntity = Mock(ResponseEntity)
mockRestTemplate.postForEntity(resourceUrl, _ as HttpEntity, Object.class) >> mockResponseEntity
when: 'POST operation is invoked'
- def result = objectUnderTest.postOperationWithJsonData(resourceUrl, 'json-data', new HttpHeaders())
+ def result = objectUnderTest.postOperationWithJsonData(resourceUrl, 'json-data')
then: 'the output of the method is equal to the output from the test template'
result == mockResponseEntity
}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy
index 3df862ac5..2a19df172 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2021-2022 Nordix Foundation
+ * Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -62,12 +63,11 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec {
and: 'a positive response from DMI service when it is called with the expected parameters'
def responseFromDmi = new ResponseEntity<Object>(HttpStatus.OK)
def expectedUrl = dmiServiceBaseUrl + "${expectedDatastoreInUrl}?resourceIdentifier=${resourceIdentifier}${expectedOptionsInUrl}"
- mockDmiRestClient.postOperationWithJsonData(expectedUrl,
- expectedJson, [Accept: ['sample accept header']]) >> responseFromDmi
+ mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson) >> responseFromDmi
dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl
when: 'get resource data is invoked'
def result = objectUnderTest.getResourceDataFromDmi(cmHandleId, resourceIdentifier,
- options, 'sample accept header', dataStore, NO_REQUEST_ID, NO_TOPIC)
+ options, dataStore, NO_REQUEST_ID, NO_TOPIC)
then: 'the result is the response from the DMI service'
assert result == responseFromDmi
where: 'the following parameters are used'
@@ -88,7 +88,7 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec {
def expectedJson = '{"operation":"' + expectedOperationInUrl + '","dataType":"some data type","data":"requestData","cmHandleProperties":{"prop1":"val1"}}'
def responseFromDmi = new ResponseEntity<Object>(HttpStatus.OK)
dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl
- mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson, [:]) >> responseFromDmi
+ mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson) >> responseFromDmi
when: 'write resource method is invoked'
def result = objectUnderTest.writeResourceDataPassThroughRunningFromDmi(cmHandleId, 'parent/child', operation, 'requestData', 'some data type')
then: 'the result is the response from the DMI service'
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy
index d3fc17cc0..574f609e9 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2021-2022 Nordix Foundation
+ * Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -55,7 +56,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec {
def moduleReferencesAsLisOfMaps = [[moduleName: 'mod1', revision: 'A'], [moduleName: 'mod2', revision: 'X']]
def expectedUrl = "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/modules"
def responseFromDmi = new ResponseEntity([schemas: moduleReferencesAsLisOfMaps], HttpStatus.OK)
- mockDmiRestClient.postOperationWithJsonData(expectedUrl, '{"cmHandleProperties":{}}', [:])
+ mockDmiRestClient.postOperationWithJsonData(expectedUrl, '{"cmHandleProperties":{}}')
>> responseFromDmi
when: 'get module references is called'
def result = objectUnderTest.getModuleReferences(yangModelCmHandle)
@@ -88,7 +89,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec {
and: 'a positive response from DMI service when it is called with tha expected parameters'
def responseFromDmi = new ResponseEntity<String>(HttpStatus.OK)
mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/modules",
- '{"cmHandleProperties":' + expectedAdditionalPropertiesInRequest + '}', [:]) >> responseFromDmi
+ '{"cmHandleProperties":' + expectedAdditionalPropertiesInRequest + '}') >> responseFromDmi
when: 'a get module references is called'
def result = objectUnderTest.getModuleReferences(yangModelCmHandle)
then: 'the result is the response from DMI service'
@@ -107,7 +108,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec {
[moduleName: 'mod2', revision: 'C', yangSource: 'other yang source']], HttpStatus.OK)
def expectedModuleReferencesInRequest = '{"name":"mod1","revision":"A"},{"name":"mod2","revision":"X"}'
mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/moduleResources",
- '{"data":{"modules":[' + expectedModuleReferencesInRequest + ']},"cmHandleProperties":{}}', [:]) >> responseFromDmi
+ '{"data":{"modules":[' + expectedModuleReferencesInRequest + ']},"cmHandleProperties":{}}') >> responseFromDmi
when: 'get new yang resources from DMI service'
def result = objectUnderTest.getNewYangResourcesFromDmi(yangModelCmHandle, newModuleReferences)
then: 'the result has the 2 expected yang (re)sources (order is not guaranteed)'
@@ -139,8 +140,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec {
and: 'a positive response from DMI service when it is called with the expected parameters'
def responseFromDmi = new ResponseEntity<>([[moduleName: 'mod1', revision: 'A', yangSource: 'some yang source']], HttpStatus.OK)
mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/moduleResources",
- '{"data":{"modules":[' + expectedModuleReferencesInRequest + ']},"cmHandleProperties":'+expectedAdditionalPropertiesInRequest+'}',
- [:]) >> responseFromDmi
+ '{"data":{"modules":[' + expectedModuleReferencesInRequest + ']},"cmHandleProperties":'+expectedAdditionalPropertiesInRequest+'}') >> responseFromDmi
when: 'get new yang resources from DMI service'
def result = objectUnderTest.getNewYangResourcesFromDmi(yangModelCmHandle, unknownModuleReferences)
then: 'the result is the response from DMI service'
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy
index e6f63ce1a..563116f40 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy
@@ -48,7 +48,7 @@ abstract class DmiOperationsBaseSpec extends Specification {
def yangModelCmHandle = new YangModelCmHandle()
def static dmiServiceName = 'some service name'
- def static cmHandleId = 'some cm handle'
+ def static cmHandleId = 'some-cm-handle'
def static resourceIdentifier = 'parent/child'
def mockYangModelCmHandleRetrieval(dmiProperties) {
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetrieverSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetrieverSpec.groovy
index 593a6ec93..bc30c9c77 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetrieverSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetrieverSpec.groovy
@@ -22,6 +22,7 @@ package org.onap.cps.ncmp.api.impl.operations
import org.onap.cps.api.CpsDataService
import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
+import org.onap.cps.spi.exceptions.DataValidationException
import spock.lang.Shared
import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
@@ -34,19 +35,19 @@ class YangModelCmHandleRetrieverSpec extends Specification {
def objectUnderTest = new YangModelCmHandleRetriever(mockCpsDataService)
- def cmHandleId = 'some cm handle'
+ def cmHandleId = 'some-cm-handle'
def leaves = ["dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
- def xpath = "/dmi-registry/cm-handles[@id='some cm handle']"
+ def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
@Shared
def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
@Shared
- def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
+ def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
@Shared
- def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
+ def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
def "Retrieve CmHandle using datanode with #scenario."() {
given: 'the cps data service returns a data node from the DMI registry'
@@ -69,4 +70,13 @@ class YangModelCmHandleRetrieverSpec extends Specification {
'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || []
'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")]
}
+
+ def "Retrieve CmHandle using datanode with invalid CmHandle id."() {
+ when: 'retrieving the yang modelled cm handle with an invalid id'
+ def result = objectUnderTest.getDmiServiceNamesAndProperties('cm handle id with spaces')
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the result is not returned'
+ result == null
+ }
}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplModelSyncSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncServiceSpec.groovy
index 553ac7279..37fdbeeb2 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplModelSyncSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncServiceSpec.groovy
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021-2022 Nordix Foundation
+ * Copyright (C) 2022 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,32 +18,22 @@
* ============LICENSE_END=========================================================
*/
-package org.onap.cps.ncmp.api.impl
+package org.onap.cps.ncmp.api.inventory.sync
-import org.onap.cps.api.CpsAdminService
import org.onap.cps.api.CpsModuleService
-import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations
-import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever
import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
import org.onap.cps.spi.model.ModuleReference
-import org.onap.cps.utils.JsonObjectMapper
import spock.lang.Specification
-class NetworkCmProxyDataServiceImplModelSyncSpec extends Specification {
+class ModuleSyncServiceSpec extends Specification {
+
- def nullCpsDataService = null
- def mockJsonObjectMapper = Mock(JsonObjectMapper)
def mockCpsModuleService = Mock(CpsModuleService)
- def mockCpsAdminService = Mock(CpsAdminService)
def mockDmiModelOperations = Mock(DmiModelOperations)
- def mockDmiDataOperations = Mock(DmiDataOperations)
- def mockYangModelCmHandleRetriever = Mock(YangModelCmHandleRetriever)
- def nullNetworkCmProxyDataServicePropertyHandler = null
- def objectUnderTest = new NetworkCmProxyDataServiceImpl(nullCpsDataService, mockJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations,
- mockCpsModuleService, mockCpsAdminService, nullNetworkCmProxyDataServicePropertyHandler,mockYangModelCmHandleRetriever)
+ def objectUnderTest = new ModuleSyncService(mockDmiModelOperations, mockCpsModuleService)
def expectedDataspaceName = 'NFP-Operational'
@@ -51,7 +41,7 @@ class NetworkCmProxyDataServiceImplModelSyncSpec extends Specification {
given: 'a cm handle'
def ncmpServiceCmHandle = new NcmpServiceCmHandle()
def dmiServiceName = 'some service name'
- ncmpServiceCmHandle.cmHandleID = 'cm handle id 1'
+ ncmpServiceCmHandle.cmHandleId = 'cmHandleId-1'
def yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle(dmiServiceName, '' , '', ncmpServiceCmHandle)
and: 'DMI operations returns some module references'
def moduleReferences = [ new ModuleReference(moduleName:'module1',revision:'1'),
@@ -63,16 +53,14 @@ class NetworkCmProxyDataServiceImplModelSyncSpec extends Specification {
mockDmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle, [new ModuleReference('module1', '1')]) >> yangResourceToContentMap
when: 'module sync is triggered'
mockCpsModuleService.identifyNewModuleReferences(moduleReferences) >> toModuleReference(identifiedNewModuleReferences)
- objectUnderTest.syncModulesAndCreateAnchor(yangModelCmHandle)
- then: 'the CPS module service is called once with the correct parameters'
- 1 * mockCpsModuleService.createSchemaSetFromModules(expectedDataspaceName, yangModelCmHandle.getId(), yangResourceToContentMap, toModuleReference(expectedKnownModules))
- and: 'admin service create anchor method has been called with correct parameters'
- 1 * mockCpsAdminService.createAnchor(expectedDataspaceName, yangModelCmHandle.getId(), yangModelCmHandle.getId())
+ def result = objectUnderTest.syncAndCreateSchemaSet(yangModelCmHandle)
+ then: 'the resulting schema set name is the same as the cm handle id'
+ assert result == 'cmHandleId-1'
where: 'the following parameters are used'
- scenario | existingModuleResourcesInCps | identifiedNewModuleReferences | yangResourceToContentMap || expectedKnownModules
- 'one new module' | [['module2' : '2'], ['module3' : '3']] | [['module1' : '1']] | [module1: 'some yang source'] || [['module2' : '2']]
- 'no add. properties' | [['module2' : '2'], ['module3' : '3']] | [['module1' : '1']] | [module1: 'some yang source'] || [['module2' : '2']]
- 'no new module' | [['module1' : '1'], ['module2' : '2']] | [] | [:] || [['module1' : '1'], ['module2' : '2']]
+ scenario | existingModuleResourcesInCps | identifiedNewModuleReferences | yangResourceToContentMap
+ 'one new module' | [['module2' : '2'], ['module3' : '3']] | [['module1' : '1']] | [module1: 'some yang source']
+ 'no add. properties' | [['module2' : '2'], ['module3' : '3']] | [['module1' : '1']] | [module1: 'some yang source']
+ 'no new module' | [['module1' : '1'], ['module2' : '2']] | [] | [:]
}
def toModuleReference(moduleReferenceAsMap) {
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy
new file mode 100644
index 000000000..4476998d8
--- /dev/null
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy
@@ -0,0 +1,71 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Bell Canada
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.models
+
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status
+import spock.lang.Specification
+
+class CmHandleRegistrationResponseSpec extends Specification {
+
+ def 'Successful cm-handle Registration Response'() {
+ when: 'cm-handle response is created'
+ def cmHandleRegistrationResponse = CmHandleRegistrationResponse.createSuccessResponse('cmHandle')
+ then: 'a success response is returned'
+ with(cmHandleRegistrationResponse) {
+ assert it.cmHandle == 'cmHandle'
+ assert it.status == Status.SUCCESS
+ }
+ and: 'error details are null'
+ cmHandleRegistrationResponse.registrationError == null
+ cmHandleRegistrationResponse.errorText == null
+ }
+
+ def 'Failed cm-handle Registration Response: for unexpected exception'() {
+ when: 'cm-handle response is created for an unexpected exception'
+ def cmHandleRegistrationResponse =
+ CmHandleRegistrationResponse.createFailureResponse('cmHandle', new Exception('unexpected error'))
+ then: 'the response is created with expected value'
+ with(cmHandleRegistrationResponse) {
+ assert it.registrationError == RegistrationError.UNKNOWN_ERROR
+ assert it.cmHandle == 'cmHandle'
+ assert errorText == 'unexpected error'
+ }
+ }
+
+ def 'Failed cm-handle Registration Response: for #scenario'() {
+ when: 'cm-handle failure response is created for #scenario'
+ def cmHandleRegistrationResponse =
+ CmHandleRegistrationResponse.createFailureResponse(cmHandleId, registrationError)
+ then: 'the response is created with expected value'
+ with(cmHandleRegistrationResponse) {
+ assert it.registrationError == registrationError
+ assert it.cmHandle == cmHandleId
+ assert it.status == Status.FAILURE
+ assert errorText == registrationError.errorText
+ }
+ where:
+ scenario | cmHandleId | registrationError
+ 'cm-handle already exists' | 'cmHandle' | RegistrationError.CM_HANDLE_ALREADY_EXIST
+ 'cm-handle id is invalid' | 'cm handle' | RegistrationError.CM_HANDLE_INVALID_ID
+ }
+
+}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/YangModelCmHandleSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/YangModelCmHandleSpec.groovy
index 470015ec1..7bbc3d753 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/YangModelCmHandleSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/YangModelCmHandleSpec.groovy
@@ -31,6 +31,7 @@ class YangModelCmHandleSpec extends Specification {
def 'Creating yang model cm handle from a service api cm handle.'() {
given: 'a cm handle with properties'
def ncmpServiceCmHandle = new NcmpServiceCmHandle()
+ ncmpServiceCmHandle.cmHandleId = 'cm-handle-id01'
ncmpServiceCmHandle.dmiProperties = [myDmiProperty:'value1']
ncmpServiceCmHandle.publicProperties = [myPublicProperty:'value2']
when: 'it is converted to a yang model cm handle'
@@ -47,7 +48,7 @@ class YangModelCmHandleSpec extends Specification {
def 'Resolve DMI service name: #scenario and #requiredService service require.'() {
given: 'a yang model cm handle'
- def objectUnderTest = YangModelCmHandle.toYangModelCmHandle(dmiServiceName, dmiDataServiceName, dmiModelServiceName, new NcmpServiceCmHandle())
+ def objectUnderTest = YangModelCmHandle.toYangModelCmHandle(dmiServiceName, dmiDataServiceName, dmiModelServiceName, new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1'))
expect:
assert objectUnderTest.resolveDmiServiceName(requiredService) == expectedService
where:
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy
index 1615d055d..4c8dcace7 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy
@@ -32,21 +32,21 @@ import spock.lang.Specification
class DmiServiceUrlBuilderSpec extends Specification {
@Shared
- YangModelCmHandle yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle("dmiServiceName",
- "dmiDataServiceName", "dmiModuleServiceName", new NcmpServiceCmHandle())
+ YangModelCmHandle yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle('dmiServiceName',
+ 'dmiDataServiceName', 'dmiModuleServiceName', new NcmpServiceCmHandle(cmHandleId: 'some-cm-handle-id'))
- NcmpConfiguration.DmiProperties dmiProperties = new NcmpConfiguration.DmiProperties();
+ NcmpConfiguration.DmiProperties dmiProperties = new NcmpConfiguration.DmiProperties()
def objectUnderTest = new DmiServiceUrlBuilder(dmiProperties)
def 'Create the dmi service url with #scenario.'() {
given: 'uri variables'
- dmiProperties.dmiBasePath = 'dmi';
+ dmiProperties.dmiBasePath = 'dmi'
def uriVars = objectUnderTest.populateUriVariables(yangModelCmHandle,
- "cmHandle", PASSTHROUGH_RUNNING);
+ "cmHandle", PASSTHROUGH_RUNNING)
and: 'query params'
def uriQueries = objectUnderTest.populateQueryParams(resourceId,
- 'optionsParamInQuery', topicParamInQuery);
+ 'optionsParamInQuery', topicParamInQuery)
when: 'a dmi datastore service url is generated'
def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars)
then: 'service url is generated as expected'
@@ -61,12 +61,12 @@ class DmiServiceUrlBuilderSpec extends Specification {
def 'Populate dmi data store url #scenario.'() {
given: 'uri variables are created'
- dmiProperties.dmiBasePath = dmiBasePath;
+ dmiProperties.dmiBasePath = dmiBasePath
def uriVars = objectUnderTest.populateUriVariables(yangModelCmHandle,
- "cmHandle", PASSTHROUGH_RUNNING);
+ "cmHandle", PASSTHROUGH_RUNNING)
and: 'null query params'
def uriQueries = objectUnderTest.populateQueryParams(null,
- null, null);
+ null, null)
when: 'a dmi datastore service url is generated'
def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars)
then: 'the created dmi service url matches the expected'
diff --git a/cps-parent/pom.xml b/cps-parent/pom.xml
index e03dce3db..b76c63c6f 100755
--- a/cps-parent/pom.xml
+++ b/cps-parent/pom.xml
@@ -32,7 +32,7 @@
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
@@ -115,7 +115,7 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
- <version>2.3.3.RELEASE</version>
+ <version>2.6.4</version>
<executions>
<execution>
<goals>
diff --git a/cps-path-parser/pom.xml b/cps-path-parser/pom.xml
index c8b88e8aa..1b0ebe2ef 100644
--- a/cps-path-parser/pom.xml
+++ b/cps-path-parser/pom.xml
@@ -23,7 +23,7 @@
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
@@ -34,6 +34,7 @@
<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
+ <version>4.9.2</version>
<executions>
<execution>
<goals>
diff --git a/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 b/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4
index cefeac438..40ad410a0 100644
--- a/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4
+++ b/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021 Nordix Foundation
+ * Copyright (C) 2021-2022 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
grammar CpsPath ;
-cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? ancestorAxis? ;
+cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? ancestorAxis? invalidPostFix?;
ancestorAxis : SLASH KW_ANCESTOR COLONCOLON ancestorPath ;
@@ -46,6 +46,8 @@ leafCondition : AT leafName EQ ( IntegerLiteral | StringLiteral) ;
leafName : QName ;
+invalidPostFix : (AT | CB | COLONCOLON | EQ ).+ ;
+
/*
* Lexer Rules
* Most of the lexer rules below are inspired by
diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java
index ebf6fd3c9..21f5173a9 100644
--- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java
+++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021 Nordix Foundation
+ * Copyright (C) 2021-2022 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import static org.onap.cps.cpspath.parser.CpsPathPrefixType.DESCENDANT;
import java.util.HashMap;
import java.util.Map;
import org.onap.cps.cpspath.parser.antlr4.CpsPathBaseListener;
+import org.onap.cps.cpspath.parser.antlr4.CpsPathParser;
import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.AncestorAxisContext;
import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.DescendantContext;
import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.IncorrectPrefixContext;
@@ -35,18 +36,33 @@ import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.TextFunctionConditionCon
public class CpsPathBuilder extends CpsPathBaseListener {
+ private static final String OPEN_BRACKET = "[";
+
+ private static final String CLOSE_BRACKET = "]";
+
final CpsPathQuery cpsPathQuery = new CpsPathQuery();
final Map<String, Object> leavesData = new HashMap<>();
+ final StringBuilder normalizedXpathBuilder = new StringBuilder();
+
+ final StringBuilder normalizedAncestorPathBuilder = new StringBuilder();
+
+ boolean processingAncestorAxis = false;
+
+ @Override
+ public void exitInvalidPostFix(final CpsPathParser.InvalidPostFixContext ctx) {
+ throw new PathParsingException(ctx.getText());
+ }
+
@Override
public void exitPrefix(final PrefixContext ctx) {
- cpsPathQuery.setXpathPrefix(ctx.getText());
+ cpsPathQuery.setXpathPrefix(normalizedXpathBuilder.toString());
}
@Override
public void exitIncorrectPrefix(final IncorrectPrefixContext ctx) {
- throw new IllegalStateException("CPS path can only start with one or two slashes (/)");
+ throw new PathParsingException("CPS path can only start with one or two slashes (/)");
}
@Override
@@ -56,32 +72,49 @@ public class CpsPathBuilder extends CpsPathBaseListener {
comparisonValue = Integer.valueOf(ctx.IntegerLiteral().getText());
}
if (ctx.StringLiteral() != null) {
+ final boolean wasWrappedInDoubleQuote = ctx.StringLiteral().getText().startsWith("\"");
comparisonValue = stripFirstAndLastCharacter(ctx.StringLiteral().getText());
+ if (wasWrappedInDoubleQuote) {
+ comparisonValue = String.valueOf(comparisonValue).replace("'", "\\'");
+ }
} else if (comparisonValue == null) {
- throw new IllegalStateException("Unsupported comparison value encountered in expression" + ctx.getText());
+ throw new PathParsingException("Unsupported comparison value encountered in expression" + ctx.getText());
}
leavesData.put(ctx.leafName().getText(), comparisonValue);
+ appendCondition(normalizedXpathBuilder, ctx.leafName().getText(), comparisonValue);
+ if (processingAncestorAxis) {
+ appendCondition(normalizedAncestorPathBuilder, ctx.leafName().getText(), comparisonValue);
+ }
}
@Override
public void exitDescendant(final DescendantContext ctx) {
cpsPathQuery.setCpsPathPrefixType(DESCENDANT);
- cpsPathQuery.setDescendantName(ctx.getText().substring(2));
+ cpsPathQuery.setDescendantName(normalizedXpathBuilder.substring(1));
+ normalizedXpathBuilder.insert(0, "/");
}
@Override
public void enterMultipleLeafConditions(final MultipleLeafConditionsContext ctx) {
+ normalizedXpathBuilder.append(OPEN_BRACKET);
leavesData.clear();
}
@Override
public void exitMultipleLeafConditions(final MultipleLeafConditionsContext ctx) {
+ normalizedXpathBuilder.append(CLOSE_BRACKET);
cpsPathQuery.setLeavesData(leavesData);
}
@Override
+ public void enterAncestorAxis(final AncestorAxisContext ctx) {
+ processingAncestorAxis = true;
+ }
+
+ @Override
public void exitAncestorAxis(final AncestorAxisContext ctx) {
- cpsPathQuery.setAncestorSchemaNodeIdentifier(ctx.ancestorPath().getText());
+ cpsPathQuery.setAncestorSchemaNodeIdentifier(normalizedAncestorPathBuilder.substring(1));
+ processingAncestorAxis = false;
}
@Override
@@ -90,7 +123,24 @@ public class CpsPathBuilder extends CpsPathBaseListener {
cpsPathQuery.setTextFunctionConditionValue(stripFirstAndLastCharacter(ctx.StringLiteral().getText()));
}
+ @Override
+ public void enterListElementRef(final CpsPathParser.ListElementRefContext ctx) {
+ normalizedXpathBuilder.append(OPEN_BRACKET);
+ if (processingAncestorAxis) {
+ normalizedAncestorPathBuilder.append(OPEN_BRACKET);
+ }
+ }
+
+ @Override
+ public void exitListElementRef(final CpsPathParser.ListElementRefContext ctx) {
+ normalizedXpathBuilder.append(CLOSE_BRACKET);
+ if (processingAncestorAxis) {
+ normalizedAncestorPathBuilder.append(CLOSE_BRACKET);
+ }
+ }
+
CpsPathQuery build() {
+ cpsPathQuery.setNormalizedXpath(normalizedXpathBuilder.toString());
return cpsPathQuery;
}
@@ -98,4 +148,23 @@ public class CpsPathBuilder extends CpsPathBaseListener {
return wrappedString.substring(1, wrappedString.length() - 1);
}
+ @Override
+ public void exitContainerName(final CpsPathParser.ContainerNameContext ctx) {
+ normalizedXpathBuilder.append("/")
+ .append(ctx.getText());
+ if (processingAncestorAxis) {
+ normalizedAncestorPathBuilder.append("/").append(ctx.getText());
+ }
+ }
+
+ private void appendCondition(final StringBuilder currentNormalizedPathBuilder, final String name,
+ final Object value) {
+ final char lastCharacter = currentNormalizedPathBuilder.charAt(currentNormalizedPathBuilder.length() - 1);
+ currentNormalizedPathBuilder.append(lastCharacter == '[' ? "" : " and ")
+ .append("@")
+ .append(name)
+ .append("='")
+ .append(value)
+ .append("'");
+ }
}
diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java
index de7adf2b7..53490f3a2 100644
--- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java
+++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021 Nordix Foundation
+ * Copyright (C) 2021-2022 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,19 +26,13 @@ import java.util.Map;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
-import org.antlr.v4.runtime.BaseErrorListener;
-import org.antlr.v4.runtime.CharStreams;
-import org.antlr.v4.runtime.CommonTokenStream;
-import org.antlr.v4.runtime.RecognitionException;
-import org.antlr.v4.runtime.Recognizer;
-import org.onap.cps.cpspath.parser.antlr4.CpsPathLexer;
-import org.onap.cps.cpspath.parser.antlr4.CpsPathParser;
@Getter
@Setter(AccessLevel.PACKAGE)
public class CpsPathQuery {
private String xpathPrefix;
+ private String normalizedXpath;
private CpsPathPrefixType cpsPathPrefixType = ABSOLUTE;
private String descendantName;
private Map<String, Object> leavesData;
@@ -53,20 +47,7 @@ public class CpsPathQuery {
* @return a CpsPathQuery object.
*/
public static CpsPathQuery createFrom(final String cpsPathSource) {
- final var inputStream = CharStreams.fromString(cpsPathSource);
- final var cpsPathLexer = new CpsPathLexer(inputStream);
- final var cpsPathParser = new CpsPathParser(new CommonTokenStream(cpsPathLexer));
- cpsPathParser.addErrorListener(new BaseErrorListener() {
- @Override
- public void syntaxError(final Recognizer<?, ?> recognizer, final Object offendingSymbol, final int line,
- final int charPositionInLine, final String msg, final RecognitionException e) {
- throw new IllegalStateException("failed to parse at line " + line + " due to " + msg, e);
- }
- });
- final var cpsPathBuilder = new CpsPathBuilder();
- cpsPathParser.addParseListener(cpsPathBuilder);
- cpsPathParser.cpsPath();
- return cpsPathBuilder.build();
+ return CpsPathUtil.getCpsPathQuery(cpsPathSource);
}
/**
diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java
new file mode 100644
index 000000000..97d7d1d76
--- /dev/null
+++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java
@@ -0,0 +1,81 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.cpspath.parser;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.onap.cps.cpspath.parser.antlr4.CpsPathLexer;
+import org.onap.cps.cpspath.parser.antlr4.CpsPathParser;
+
+@Getter
+@Setter
+@NoArgsConstructor(access = AccessLevel.PACKAGE)
+public class CpsPathUtil {
+
+ /**
+ * Returns a normalized xpath path query.
+ *
+ * @param xpathSource xpath
+ * @return a normalized xpath String.
+ */
+ public static String getNormalizedXpath(final String xpathSource) {
+ final CpsPathBuilder cpsPathBuilder = getCpsPathBuilder(xpathSource);
+ return cpsPathBuilder.build().getNormalizedXpath();
+ }
+
+ /**
+ * Returns a cps path query.
+ *
+ * @param cpsPathSource cps path
+ * @return a CpsPathQuery object.
+ */
+
+ public static CpsPathQuery getCpsPathQuery(final String cpsPathSource) {
+ final CpsPathBuilder cpsPathBuilder = getCpsPathBuilder(cpsPathSource);
+ return cpsPathBuilder.build();
+ }
+
+ private static CpsPathBuilder getCpsPathBuilder(final String cpsPathSource) {
+ final CharStream inputStream = CharStreams.fromString(cpsPathSource);
+ final CpsPathLexer cpsPathLexer = new CpsPathLexer(inputStream);
+ final CpsPathParser cpsPathParser = new CpsPathParser(new CommonTokenStream(cpsPathLexer));
+ cpsPathParser.addErrorListener(new BaseErrorListener() {
+ @Override
+ public void syntaxError(final Recognizer<?, ?> recognizer, final Object offendingSymbol, final int line,
+ final int charPositionInLine, final String msg, final RecognitionException e) {
+ throw new PathParsingException("failed to parse at line " + line + " due to " + msg,
+ e == null ? "" : e.getMessage());
+ }
+ });
+ final CpsPathBuilder cpsPathBuilder = new CpsPathBuilder();
+ cpsPathParser.addParseListener(cpsPathBuilder);
+ cpsPathParser.cpsPath();
+ return cpsPathBuilder;
+ }
+}
diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java
new file mode 100755
index 000000000..4a67167c9
--- /dev/null
+++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java
@@ -0,0 +1,55 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.cpspath.parser;
+
+import lombok.Getter;
+
+/**
+ * XPath Parsing Exception.
+ */
+public class PathParsingException extends RuntimeException {
+
+ private static final long serialVersionUID = 7072864354925271894L;
+
+ @Getter
+ final String details;
+
+ /**
+ * Constructor.
+ *
+ * @param details the error details
+ */
+ public PathParsingException(final String details) {
+ super("Error while parsing xpath expression");
+ this.details = details;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message the error message
+ * @param details the error details
+ */
+ public PathParsingException(final String message, final String details) {
+ super(message);
+ this.details = details;
+ }
+}
diff --git a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy
index bfec574eb..b837a64fe 100644
--- a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy
+++ b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021 Nordix Foundation
+ * Copyright (C) 2021-2022 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -34,17 +34,17 @@ class CpsPathQuerySpec extends Specification {
result.cpsPathPrefixType == ABSOLUTE
and: 'the right query parameters are set'
result.xpathPrefix == expectedXpathPrefix
- result.hasLeafConditions() == true
- result.leavesData.containsKey(expectedLeafName) == true
+ result.hasLeafConditions()
+ result.leavesData.containsKey(expectedLeafName)
result.leavesData.get(expectedLeafName) == expectedLeafValue
where: 'the following data is used'
- scenario | cpsPath || expectedXpathPrefix | expectedLeafName | expectedLeafValue
- 'leaf of type String' | '/parent/child[@common-leaf-name="common-leaf-value"]' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value'
- 'leaf of type String' | '/parent/child[@common-leaf-name=\'common-leaf-value\']' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value'
- 'leaf of type Integer' | '/parent/child[@common-leaf-name-int=5]' || '/parent/child' | 'common-leaf-name-int' | 5
- 'spaces around =' | '/parent/child[@common-leaf-name-int = 5]' || '/parent/child' | 'common-leaf-name-int' | 5
- 'key in top container' | '/parent[@common-leaf-name-int=5]' || '/parent' | 'common-leaf-name-int' | 5
- 'parent list' | '/shops/shop[@id=1]/categories[@id=1]/book[@title="Dune"]' || '/shops/shop[@id=1]/categories[@id=1]/book' | 'title' | 'Dune'
+ scenario | cpsPath || expectedXpathPrefix | expectedLeafName | expectedLeafValue
+ 'leaf of type String' | '/parent/child[@common-leaf-name="common-leaf-value"]' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value'
+ 'leaf of type String' | '/parent/child[@common-leaf-name=\'common-leaf-value\']' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value'
+ 'leaf of type Integer' | '/parent/child[@common-leaf-name-int=5]' || '/parent/child' | 'common-leaf-name-int' | 5
+ 'spaces around =' | '/parent/child[@common-leaf-name-int = 5]' || '/parent/child' | 'common-leaf-name-int' | 5
+ 'key in top container' | '/parent[@common-leaf-name-int=5]' || '/parent' | 'common-leaf-name-int' | 5
+ 'parent list' | '/shops/shop[@id=1]/categories[@id=1]/book[@title="Dune"]' || "/shops/shop[@id='1']/categories[@id='1']/book" | 'title' | 'Dune'
}
def 'Parse cps path of type ends with a #scenario.'() {
@@ -60,6 +60,38 @@ class CpsPathQuerySpec extends Specification {
'parent & child' | '//parent/child' || 'parent/child'
}
+ def 'Parse cps path to form the Normalized cps path containing #scenario.'() {
+ when: 'the given cps path is parsed'
+ def result = CpsPathUtil.getCpsPathQuery(cpsPath)
+ then: 'the query has the right normalized xpath type'
+ assert result.normalizedXpath == expectedNormalizedXPath
+ where: 'the following data is used'
+ scenario | cpsPath || expectedNormalizedXPath
+ 'yang container' | '/cps-path' || '/cps-path'
+ 'descendant anywhere' | '//cps-path' || '//cps-path'
+ 'descendant with leaf condition' | '//cps-path[@key=1]' || "//cps-path[@key='1']"
+ 'descendant with leaf value and ancestor' | '//cps-path[@key=1]/ancestor:parent[@key=1]' || "//cps-path[@key='1']/ancestor:parent[@key='1']"
+ 'parent & child' | '/parent/child' || '/parent/child'
+ 'parent leaf of type Integer & child' | '/parent/child[@code=1]/child2' || "/parent/child[@code='1']/child2"
+ 'parent leaf with double quotes' | '/parent/child[@code="1"]/child2' || "/parent/child[@code='1']/child2"
+ 'parent leaf with double quotes inside single quotes' | '/parent/child[@code=\'"1"\']/child2' || "/parent/child[@code='\"1\"']/child2"
+ 'parent leaf with single quotes inside double quotes' | '/parent/child[@code="\'1\'"]/child2' || "/parent/child[@code='\\\'1\\\'']/child2"
+ 'leaf with single quotes inside double quotes' | '/parent/child[@code="\'1\'"]' || "/parent/child[@code='\\\'1\\\'']"
+ 'leaf with more than one attribute' | '/parent/child[@key1=1 and @key2="abc"]' || "/parent/child[@key1='1' and @key2='abc']"
+ 'parent & child with more than one attribute' | '/parent/child[@key1=1 and @key2="abc"]/child2' || "/parent/child[@key1='1' and @key2='abc']/child2"
+ }
+
+ def 'Parse xpath to form the Normalized xpath containing #scenario.'() {
+ when: 'the given xpath is parsed'
+ def result = CpsPathUtil.getNormalizedXpath(xPath)
+ then: 'the query has the right normalized xpath type'
+ assert result == expectedNormalizedXPath
+ where: 'the following data is used'
+ scenario | xPath || expectedNormalizedXPath
+ 'yang container' | '/xpath' || '/xpath'
+ 'descendant anywhere' | '//xpath' || '//xpath'
+ }
+
def 'Parse cps path that ends with a yang list containing #scenario.'() {
when: 'the given cps path is parsed'
def result = CpsPathQuery.createFrom(cpsPath)
@@ -99,7 +131,7 @@ class CpsPathQuerySpec extends Specification {
when: 'the given cps path is parsed'
CpsPathQuery.createFrom(cpsPath)
then: 'a CpsPathException is thrown'
- thrown(IllegalStateException)
+ thrown(PathParsingException)
where: 'the following data is used'
scenario | cpsPath
'no / at the start' | 'invalid-cps-path/child'
@@ -110,7 +142,9 @@ class CpsPathQuerySpec extends Specification {
'end with descendant and more than one attribute separated by "or"' | '//child[@int-leaf=5 or @leaf-name="leaf value"]'
'missing attribute value' | '//child[@int-leaf=5 and @name]'
'incomplete ancestor value' | '//books/ancestor::'
-// DISCUSS WITH TEAM : 'unsupported postfix after value condition (JIRA CPS-450)' | '/parent/child[@id=1]/somePostFix'
+ 'invalid list element with missing [' | '/parent-206/child-206/grand-child-206@key="A"]'
+ 'invalid list element with incorrect ]' | '/parent-206/child-206/grand-child-206]@key="A"]'
+ 'invalid list element with incorrect ::' | '/parent-206/child-206/grand-child-206::@key"A"]'
}
def 'Parse cps path using ancestor by schema node identifier with a #scenario.'() {
@@ -125,11 +159,12 @@ class CpsPathQuerySpec extends Specification {
and: 'there are no leaves conditions'
result.hasLeafConditions() == false
where:
- scenario | ancestorPath
- 'basic container' | 'someContainer'
- 'container with parent' | 'parent/child'
- 'ancestor that is a list' | 'categories[@code=1]'
- 'parent that is a list' | 'parent[@id=1]/child'
+ scenario | ancestorPath
+ 'basic container' | 'someContainer'
+ 'container with parent' | 'parent/child'
+ 'ancestor that is a list' | "categories[@code='1']"
+ 'ancestor that is a list with compound key' | "categories[@key1='1' and @key2='2']"
+ 'parent that is a list' | "parent[@id='1']/child"
}
def 'Combinations #scenario.'() {
@@ -145,11 +180,10 @@ class CpsPathQuerySpec extends Specification {
result.ancestorSchemaNodeIdentifier == 'someAncestor'
result.descendantName == expectedDescendantName
where:
- scenario | cpsPath || expectedDescendantName | expectLeafConditions
- 'basic container' | '//someContainer' || 'someContainer' | false
- 'container with parent' | '//parent/child' || 'parent/child' | false
- 'container with list-parent' | '//parent[@id=1]/child' || 'parent[@id=1]/child' | false
- 'container with list-parent' | '//parent[@id=1]/child[@name="test"]' || 'parent[@id=1]/child' | true
+ scenario | cpsPath || expectedDescendantName | expectLeafConditions
+ 'basic container' | '//someContainer' || 'someContainer' | false
+ 'container with parent' | '//parent/child' || 'parent/child' | false
+ 'container with list-parent' | '//parent[@id=1]/child' || "parent[@id='1']/child" | false
+ 'container with list-parent' | '//parent[@id=1]/child[@name="test"]' || "parent[@id='1']/child" | true
}
-
}
diff --git a/cps-rest/docs/openapi/cpsAdmin.yml b/cps-rest/docs/openapi/cpsAdmin.yml
index a25f81eaf..5852c0cf1 100644
--- a/cps-rest/docs/openapi/cpsAdmin.yml
+++ b/cps-rest/docs/openapi/cpsAdmin.yml
@@ -29,6 +29,8 @@ dataspaces:
responses:
'201':
$ref: 'components.yml#/components/responses/Created'
+ '400':
+ $ref: 'components.yml#/components/responses/BadRequest'
'401':
$ref: 'components.yml#/components/responses/Unauthorized'
'403':
diff --git a/cps-rest/pom.xml b/cps-rest/pom.xml
index 5a21957cd..601919726 100755
--- a/cps-rest/pom.xml
+++ b/cps-rest/pom.xml
@@ -28,7 +28,7 @@
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
index 58a5ebf04..41ad9ca5b 100755
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
@@ -22,14 +22,13 @@
package org.onap.cps.rest.controller
-import org.mapstruct.factory.Mappers
-
import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
+import org.mapstruct.factory.Mappers
import org.onap.cps.api.CpsAdminService
import org.onap.cps.api.CpsModuleService
import org.onap.cps.spi.exceptions.AlreadyDefinedException
@@ -73,7 +72,7 @@ class AdminRestControllerSpec extends Specification {
def 'Create new dataspace.'() {
given: 'an endpoint'
- def createDataspaceEndpoint = "$basePath/v1/dataspaces";
+ def createDataspaceEndpoint = "$basePath/v1/dataspaces"
when: 'post is invoked'
def response =
mvc.perform(
@@ -88,7 +87,7 @@ class AdminRestControllerSpec extends Specification {
def 'Create dataspace over existing with same name.'() {
given: 'an endpoint'
- def createDataspaceEndpoint = "$basePath/v1/dataspaces";
+ def createDataspaceEndpoint = "$basePath/v1/dataspaces"
and: 'the service method throws an exception indicating the dataspace is already defined'
def thrownException = new AlreadyDefinedException(dataspaceName, new RuntimeException())
mockCpsAdminService.createDataspace(dataspaceName) >> { throw thrownException }
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
index 2aa4ddd1e..d4c68c30a 100644
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
@@ -24,7 +24,6 @@ package org.onap.cps.rest.exceptions
import com.fasterxml.jackson.databind.ObjectMapper
import groovy.json.JsonSlurper
-import org.mapstruct.factory.Mappers
import org.onap.cps.api.CpsAdminService
import org.onap.cps.api.CpsDataService
import org.onap.cps.api.CpsModuleService
diff --git a/cps-ri/pom.xml b/cps-ri/pom.xml
index 6e92894fc..98a392a5c 100644
--- a/cps-ri/pom.xml
+++ b/cps-ri/pom.xml
@@ -26,7 +26,7 @@
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java
index 50b27207e..2e7bb7e96 100755
--- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java
@@ -24,6 +24,7 @@ package org.onap.cps.spi.impl;
import java.util.Collection;
import java.util.List;
+import java.util.Set;
import java.util.stream.Collectors;
import javax.transaction.Transactional;
import lombok.AllArgsConstructor;
@@ -36,8 +37,10 @@ import org.onap.cps.spi.exceptions.AlreadyDefinedException;
import org.onap.cps.spi.exceptions.DataspaceInUseException;
import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException;
import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
import org.onap.cps.spi.repository.AnchorRepository;
import org.onap.cps.spi.repository.DataspaceRepository;
+import org.onap.cps.spi.repository.ModuleReferenceRepository;
import org.onap.cps.spi.repository.SchemaSetRepository;
import org.onap.cps.spi.repository.YangResourceRepository;
import org.springframework.dao.DataIntegrityViolationException;
@@ -51,6 +54,7 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic
private final AnchorRepository anchorRepository;
private final SchemaSetRepository schemaSetRepository;
private final YangResourceRepository yangResourceRepository;
+ private final ModuleReferenceRepository moduleReferenceRepository;
@Override
public void createDataspace(final String dataspaceName) {
@@ -132,6 +136,11 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic
anchorRepository.delete(anchorEntity);
}
+ @Override
+ public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) {
+ return moduleReferenceRepository.queryCmHandles(cmHandleQueryParameters);
+ }
+
private AnchorEntity getAnchorEntity(final String dataspaceName, final String anchorName) {
final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
return anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
index 78862d723..daf4dd757 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
@@ -41,6 +41,8 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.StaleStateException;
import org.onap.cps.cpspath.parser.CpsPathQuery;
+import org.onap.cps.cpspath.parser.CpsPathUtil;
+import org.onap.cps.cpspath.parser.PathParsingException;
import org.onap.cps.spi.CpsDataPersistenceService;
import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.entities.AnchorEntity;
@@ -56,6 +58,7 @@ import org.onap.cps.spi.model.DataNodeBuilder;
import org.onap.cps.spi.repository.AnchorRepository;
import org.onap.cps.spi.repository.DataspaceRepository;
import org.onap.cps.spi.repository.FragmentRepository;
+import org.onap.cps.spi.utils.SessionManager;
import org.onap.cps.utils.JsonObjectMapper;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
@@ -73,6 +76,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
private final JsonObjectMapper jsonObjectMapper;
+ private final SessionManager sessionManager;
+
private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})";
private static final Pattern REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE =
Pattern.compile("\\[(\\@([^\\/]{0,9999}))\\]$");
@@ -171,8 +176,14 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
if (isRootXpath(xpath)) {
return fragmentRepository.findFirstRootByDataspaceAndAnchor(dataspaceEntity, anchorEntity);
} else {
+ final String normalizedXpath;
+ try {
+ normalizedXpath = CpsPathUtil.getNormalizedXpath(xpath);
+ } catch (final PathParsingException e) {
+ throw new CpsPathException(e.getMessage());
+ }
return fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity,
- xpath);
+ normalizedXpath);
}
}
@@ -183,8 +194,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
final CpsPathQuery cpsPathQuery;
try {
- cpsPathQuery = CpsPathQuery.createFrom(cpsPath);
- } catch (final IllegalStateException e) {
+ cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
+ } catch (final PathParsingException e) {
throw new CpsPathException(e.getMessage());
}
List<FragmentEntity> fragmentEntities =
@@ -199,6 +210,22 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
.collect(Collectors.toUnmodifiableList());
}
+ @Override
+ public String startSession() {
+ return sessionManager.startSession();
+ }
+
+ @Override
+ public void closeSession(final String sessionId) {
+ sessionManager.closeSession(sessionId);
+ }
+
+ @Override
+ public void lockAnchor(final String sessionId, final String dataspaceName,
+ final String anchorName, final Long timeoutInMilliseconds) {
+ sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
+ }
+
private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities,
final CpsPathQuery cpsPathQuery) {
final Set<String> ancestorXpath = new HashSet<>();
@@ -365,12 +392,13 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
}
private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) {
- if (parentFragmentEntity.getXpath().equals(targetXpath)) {
+ final String normalizedTargetXpath = CpsPathUtil.getNormalizedXpath(targetXpath);
+ if (parentFragmentEntity.getXpath().equals(normalizedTargetXpath)) {
fragmentRepository.delete(parentFragmentEntity);
return true;
}
if (parentFragmentEntity.getChildFragments()
- .removeIf(fragment -> fragment.getXpath().equals(targetXpath))) {
+ .removeIf(fragment -> fragment.getXpath().equals(normalizedTargetXpath))) {
fragmentRepository.save(parentFragmentEntity);
return true;
}
@@ -378,7 +406,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
}
private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) {
- final String deleteTargetXpathPrefix = listXpath + "[";
+ final String normalizedListXpath = CpsPathUtil.getNormalizedXpath(listXpath);
+ final String deleteTargetXpathPrefix = normalizedListXpath + "[";
if (parentFragmentEntity.getChildFragments()
.removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) {
fragmentRepository.save(parentFragmentEntity);
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java
index 6551937e1..4bc9dd960 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java
@@ -21,6 +21,8 @@
package org.onap.cps.spi.repository;
import java.util.Collection;
+import java.util.Set;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
import org.onap.cps.spi.model.ModuleReference;
/**
@@ -31,4 +33,12 @@ public interface ModuleReferenceQuery {
Collection<ModuleReference> identifyNewModuleReferences(
final Collection<ModuleReference> moduleReferencesToCheck);
+ /**
+ * Query and return cm handles that match the given query parameters.
+ *
+ * @param cmHandleQueryParameters the cm handle query parameters
+ * @return collection of cm handle ids
+ */
+ Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters);
+
}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java
index ce2bfe784..f70e21837 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java
@@ -27,8 +27,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
-public interface ModuleReferenceRepository extends
- JpaRepository<YangResourceEntity, Long>, ModuleReferenceQuery {
+public interface ModuleReferenceRepository extends JpaRepository<YangResourceEntity, Long>, ModuleReferenceQuery {
Collection<ModuleReference> identifyNewModuleReferences(
final Collection<ModuleReference> moduleReferencesToCheck);
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java
index 0e79deb8e..f85dea3a7 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java
@@ -24,21 +24,32 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.UUID;
+import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
+import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.spi.CpsDataPersistenceService;
+import org.onap.cps.spi.FetchDescendantsOption;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
+import org.onap.cps.spi.model.DataNode;
import org.onap.cps.spi.model.ModuleReference;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Transactional
+@AllArgsConstructor
public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery {
@PersistenceContext
private EntityManager entityManager;
+ private final CpsDataPersistenceService cpsDataPersistenceService;
+
@Override
@SneakyThrows
public Collection<ModuleReference> identifyNewModuleReferences(
@@ -57,6 +68,56 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery {
return identifyNewModuleReferencesForCmHandle(tempTableName);
}
+ /**
+ * Query and return cm handles that match the given query parameters.
+ *
+ * @param cmHandleQueryParameters the cm handle query parameters
+ * @return collection of cm handle ids
+ */
+ @Override
+ public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) {
+
+ if (cmHandleQueryParameters.getPublicProperties().entrySet().isEmpty()) {
+ return getAllCmHandles();
+ }
+
+ final Collection<DataNode> amalgamatedQueryResult = new ArrayList<>();
+ int queryConditionCounter = 0;
+ for (final Map.Entry<String, String> entry : cmHandleQueryParameters.getPublicProperties().entrySet()) {
+ final StringBuilder cmHandlePath = new StringBuilder();
+ cmHandlePath.append("//public-properties[@name='").append(entry.getKey()).append("' ");
+ cmHandlePath.append("and @value='").append(entry.getValue()).append("']");
+ cmHandlePath.append("/ancestor::cm-handles");
+
+ final Collection<DataNode> singleConditionQueryResult =
+ cpsDataPersistenceService.queryDataNodes("NCMP-Admin",
+ "ncmp-dmi-registry", String.valueOf(cmHandlePath), FetchDescendantsOption.OMIT_DESCENDANTS);
+ if (++queryConditionCounter == 1) {
+ amalgamatedQueryResult.addAll(singleConditionQueryResult);
+ } else {
+ amalgamatedQueryResult.retainAll(singleConditionQueryResult);
+ }
+
+ if (amalgamatedQueryResult.isEmpty()) {
+ break;
+ }
+ }
+
+ return extractCmHandleIds(amalgamatedQueryResult);
+ }
+
+ private Set<String> getAllCmHandles() {
+ final Collection<DataNode> cmHandles = cpsDataPersistenceService.queryDataNodes("NCMP-Admin",
+ "ncmp-dmi-registry", "//public-properties/ancestor::cm-handles",
+ FetchDescendantsOption.OMIT_DESCENDANTS);
+ return extractCmHandleIds(cmHandles);
+ }
+
+ private Set<String> extractCmHandleIds(final Collection<DataNode> cmHandles) {
+ return cmHandles.stream().map(cmHandle -> cmHandle.getLeaves().get("id").toString())
+ .collect(Collectors.toSet());
+ }
+
private void createTemporaryTable(final String tempTableName) {
final StringBuilder sqlStringBuilder = new StringBuilder("CREATE TEMPORARY TABLE " + tempTableName + "(");
sqlStringBuilder.append(" id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,");
@@ -94,8 +155,8 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery {
+ " AND yang_resource.revision=%1$s.revision"
+ " WHERE yang_resource.module_name IS NULL;", tempTableName);
- final List<Object[]> resultsAsObjects =
- entityManager.createNativeQuery(sql).getResultList();
+ @SuppressWarnings("unchecked")
+ final List<Object[]> resultsAsObjects = entityManager.createNativeQuery(sql).getResultList();
final List<ModuleReference> resultsAsModuleReferences = new ArrayList<>(resultsAsObjects.size());
for (final Object[] row : resultsAsObjects) {
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java b/cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java
new file mode 100644
index 000000000..e2786887a
--- /dev/null
+++ b/cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java
@@ -0,0 +1,165 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.utils;
+
+import com.google.common.util.concurrent.TimeLimiter;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.HibernateException;
+import org.hibernate.LockMode;
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+import org.hibernate.cfg.Configuration;
+import org.onap.cps.spi.entities.AnchorEntity;
+import org.onap.cps.spi.entities.DataspaceEntity;
+import org.onap.cps.spi.entities.SchemaSetEntity;
+import org.onap.cps.spi.entities.YangResourceEntity;
+import org.onap.cps.spi.exceptions.SessionManagerException;
+import org.onap.cps.spi.exceptions.SessionTimeoutException;
+import org.onap.cps.spi.repository.AnchorRepository;
+import org.onap.cps.spi.repository.DataspaceRepository;
+import org.springframework.stereotype.Component;
+
+@RequiredArgsConstructor
+@Slf4j
+@Component
+public class SessionManager {
+
+ private final TimeLimiterProvider timeLimiterProvider;
+ private final DataspaceRepository dataspaceRepository;
+ private final AnchorRepository anchorRepository;
+ private static SessionFactory sessionFactory;
+ private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();
+
+ private synchronized void buildSessionFactory() {
+ if (sessionFactory == null) {
+ sessionFactory = new Configuration().configure("hibernate.cfg.xml")
+ .addAnnotatedClass(AnchorEntity.class)
+ .addAnnotatedClass(DataspaceEntity.class)
+ .addAnnotatedClass(SchemaSetEntity.class)
+ .addAnnotatedClass(YangResourceEntity.class)
+ .buildSessionFactory();
+ }
+ }
+
+ /**
+ * Starts a session which allows use of locks and batch interaction with the persistence service.
+ *
+ * @return Session ID string
+ */
+ public String startSession() {
+ buildSessionFactory();
+ final Session session = sessionFactory.openSession();
+ final String sessionId = UUID.randomUUID().toString();
+ sessionMap.put(sessionId, session);
+ session.beginTransaction();
+ return sessionId;
+ }
+
+ /**
+ * Close session.
+ * Locks will be released and changes will be committed.
+ *
+ * @param sessionId session ID
+ */
+ public void closeSession(final String sessionId) {
+ try {
+ final Session session = getSession(sessionId);
+ session.getTransaction().commit();
+ session.close();
+ } catch (final HibernateException e) {
+ throw new SessionManagerException("Cannot close session",
+ String.format("Unable to close session with session ID '%s'", sessionId), e);
+ } finally {
+ sessionMap.remove(sessionId);
+ }
+ }
+
+ /**
+ * Lock Anchor.
+ * To release locks(s), the session holding the lock(s) must be closed.
+ *
+ * @param sessionId session ID
+ * @param dataspaceName dataspace name
+ * @param anchorName anchor name
+ * @param timeoutInMilliseconds lock attempt timeout in milliseconds
+ */
+ @SneakyThrows
+ public void lockAnchor(final String sessionId, final String dataspaceName,
+ final String anchorName, final Long timeoutInMilliseconds) {
+ final ExecutorService executorService = Executors.newSingleThreadExecutor();
+ final TimeLimiter timeLimiter = timeLimiterProvider.getTimeLimiter(executorService);
+
+ try {
+ timeLimiter.callWithTimeout(() -> {
+ applyPessimisticWriteLockOnAnchor(sessionId, dataspaceName, anchorName);
+ return null;
+ }, timeoutInMilliseconds, TimeUnit.MILLISECONDS);
+ } catch (final TimeoutException e) {
+ throw new SessionTimeoutException(
+ "Timeout: Anchor locking failed",
+ "The error could be caused by another session holding a lock on the specified table. "
+ + "Retrying the sending the request could be required.", e);
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new SessionManagerException("Operation interrupted", "This thread was interrupted.", e);
+ } catch (final ExecutionException | UncheckedExecutionException e) {
+ if (e.getCause() != null) {
+ throw e.getCause();
+ }
+ throw new SessionManagerException(
+ "Operation Aborted",
+ "The transaction request was aborted. "
+ + "Retrying and checking all details are correct could be required", e);
+ } finally {
+ executorService.shutdownNow();
+ }
+ }
+
+ private void applyPessimisticWriteLockOnAnchor(final String sessionId, final String dataspaceName,
+ final String anchorName) {
+ final Session session = getSession(sessionId);
+ final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+ final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+ final int anchorId = anchorEntity.getId();
+ log.debug("Attempting to lock anchor {} for session {}", anchorName, sessionId);
+ session.get(AnchorEntity.class, anchorId, LockMode.PESSIMISTIC_WRITE);
+ log.info("Anchor {} successfully locked", anchorName);
+ }
+
+ private Session getSession(final String sessionId) {
+ final Session session = sessionMap.get(sessionId);
+ if (session == null) {
+ throw new SessionManagerException("Session not found",
+ String.format("Session with ID %s does not exist", sessionId));
+ }
+ return session;
+ }
+}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/utils/TimeLimiterProvider.java b/cps-ri/src/main/java/org/onap/cps/spi/utils/TimeLimiterProvider.java
new file mode 100644
index 000000000..2bd7ac376
--- /dev/null
+++ b/cps-ri/src/main/java/org/onap/cps/spi/utils/TimeLimiterProvider.java
@@ -0,0 +1,33 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.utils;
+
+import com.google.common.util.concurrent.SimpleTimeLimiter;
+import com.google.common.util.concurrent.TimeLimiter;
+import java.util.concurrent.ExecutorService;
+import org.springframework.stereotype.Component;
+
+@Component
+public class TimeLimiterProvider {
+ public TimeLimiter getTimeLimiter(final ExecutorService executorService) {
+ return SimpleTimeLimiter.create(executorService);
+ }
+}
diff --git a/cps-ri/src/main/resources/hibernate.cfg.xml b/cps-ri/src/main/resources/hibernate.cfg.xml
new file mode 100644
index 000000000..98e6cfc5b
--- /dev/null
+++ b/cps-ri/src/main/resources/hibernate.cfg.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE hibernate-configuration PUBLIC
+ "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
+ "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
+
+<hibernate-configuration>
+ <session-factory>
+ <property name="hibernate.connection.driver_class">org.postgresql.Driver</property>
+ <property name="hibernate.connection.url">jdbc:postgresql://${DB_HOST}:${DB_PORT:5432}/cpsdb</property>
+ <property name="hibernate.connection.username">${DB_USERNAME}</property>
+ <property name="hibernate.connection.password">${DB_PASSWORD}</property>
+ <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQL82Dialect</property>
+ <property name="show_sql">true</property>
+ <property name="hibernate.hbm2ddl.auto">update</property>
+ </session-factory>
+</hibernate-configuration> \ No newline at end of file
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy
index 063bd5b5a..2de087fc2 100644
--- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy
+++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021 Nordix Foundation
+ * Copyright (C) 2021-2022 Nordix Foundation
* Modifications Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
@@ -22,6 +22,7 @@
package org.onap.cps.spi.impl
+import org.mockito.Mock
import org.onap.cps.spi.CpsAdminPersistenceService
import org.onap.cps.spi.exceptions.AlreadyDefinedException
import org.onap.cps.spi.exceptions.AnchorNotFoundException
@@ -30,15 +31,21 @@ import org.onap.cps.spi.exceptions.DataspaceNotFoundException
import org.onap.cps.spi.exceptions.SchemaSetNotFoundException
import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException
import org.onap.cps.spi.model.Anchor
+import org.onap.cps.spi.model.CmHandleQueryParameters
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.jdbc.Sql
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper
class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase {
@Autowired
CpsAdminPersistenceService objectUnderTest
+ @Mock
+ ObjectMapper objectMapper
+
static final String SET_DATA = '/data/anchor.sql'
+ static final String SET_FRAGMENT_DATA = '/data/fragment.sql'
static final String SAMPLE_DATA_FOR_ANCHORS_WITH_MODULES = '/data/anchors-schemaset-modules.sql'
static final String DATASPACE_WITH_NO_DATA = 'DATASPACE-002-NO-DATA'
static final Integer DELETED_ANCHOR_ID = 3002
@@ -46,7 +53,7 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase {
@Sql(CLEAR_DATA)
def 'Create and retrieve a new dataspace.'() {
when: 'a new dataspace is created'
- def dataspaceName = 'some new dataspace'
+ def dataspaceName = 'some-new-dataspace'
objectUnderTest.createDataspace(dataspaceName)
then: 'that dataspace can be retrieved from the dataspace repository'
def dataspaceEntity = dataspaceRepository.findByName(dataspaceName).orElseThrow()
@@ -66,7 +73,7 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase {
@Sql([CLEAR_DATA, SET_DATA])
def 'Create and retrieve a new anchor.'() {
when: 'a new anchor is created'
- def newAnchorName = 'my new anchor'
+ def newAnchorName = 'my-new-anchor'
objectUnderTest.createAnchor(DATASPACE_NAME, SCHEMA_SET_NAME1, newAnchorName)
then: 'that anchor can be retrieved'
def anchor = objectUnderTest.getAnchor(DATASPACE_NAME, newAnchorName)
@@ -141,7 +148,7 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase {
@Sql(CLEAR_DATA)
def 'Get all anchors in unknown dataspace.'() {
when: 'attempt to get all anchors in an unknown dataspace'
- objectUnderTest.getAnchors('unknown dataspace')
+ objectUnderTest.getAnchors('unknown-dataspace')
then: 'an DataspaceNotFoundException is thrown'
thrown(DataspaceNotFoundException)
}
@@ -219,4 +226,20 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase {
'dataspace contains schemasets' | 'DATASPACE-003' || DataspaceInUseException | 'contains 1 schemaset(s)'
}
+ @Sql([CLEAR_DATA, SET_FRAGMENT_DATA])
+ def 'Retrieve cm handle ids when #scenario.'() {
+ when: 'the service is invoked'
+ def cmHandleQueryParameters = new CmHandleQueryParameters()
+ cmHandleQueryParameters.setPublicProperties(publicProperties)
+ def returnedCmHandles = objectUnderTest.queryCmHandles(cmHandleQueryParameters)
+ then: 'the correct expected cm handles are returned'
+ returnedCmHandles == expectedCmHandleIds
+ where: 'the following data is used'
+ scenario | publicProperties || expectedCmHandleIds
+ 'single matching property' | ['Contact' : 'newemailforstore@bookstore.com'] || ['PNFDemo2', 'PNFDemo', 'PNFDemo4'] as Set
+ 'public property dont match' | ['wont_match' : 'wont_match'] || [] as Set
+ '2 properties, only one match (and)' | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': 'newemailforstore2@bookstore.com'] || ['PNFDemo4'] as Set
+ '2 properties, no match (and)' | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': ''] || [] as Set
+ 'No public properties - return all cm handles' | [ : ] || ['PNFDemo3', 'PNFDemo', 'PNFDemo2', 'PNFDemo4'] as Set
+ }
}
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy
index ae88d302b..36b378a77 100644
--- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy
+++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021 Nordix Foundation
+ * Copyright (C) 2021-2022 Nordix Foundation
* Modifications Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2021 Bell Canada.
* ================================================================================
@@ -92,15 +92,15 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
}
where: 'the following data is used'
scenario | cpsPath || expectedXPaths
- 'fully unique descendant name' | '//categories[@code=2]' || ['/shops/shop[@id=1]/categories[@code=2]', '/shops/shop[@id=2]/categories[@code=1]', '/shops/shop[@id=2]/categories[@code=2]']
- 'descendant name match end of other node' | '//book' || ['/shops/shop[@id=1]/categories[@code=1]/book', '/shops/shop[@id=1]/categories[@code=2]/book']
- 'descendant with text condition on leaf' | '//book/title[text()="Chapters"]' || ['/shops/shop[@id=1]/categories[@code=2]/book']
+ 'fully unique descendant name' | '//categories[@code=2]' || ["/shops/shop[@id='1']/categories[@code='2']", "/shops/shop[@id='2']/categories[@code='1']", "/shops/shop[@id='2']/categories[@code='2']"]
+ 'descendant name match end of other node' | '//book' || ["/shops/shop[@id='1']/categories[@code='1']/book", "/shops/shop[@id='1']/categories[@code='2']/book"]
+ 'descendant with text condition on leaf' | '//book/title[text()="Chapters"]' || ["/shops/shop[@id='1']/categories[@code='2']/book"]
'descendant with text condition case mismatch' | '//book/title[text()="chapters"]' || []
- 'descendant with text condition on int leaf' | '//book/price[text()="5"]' || ['/shops/shop[@id=1]/categories[@code=1]/book']
- 'descendant with text condition on leaf-list' | '//book/labels[text()="special offer"]' || ['/shops/shop[@id=1]/categories[@code=1]/book']
+ 'descendant with text condition on int leaf' | '//book/price[text()="5"]' || ["/shops/shop[@id='1']/categories[@code='1']/book"]
+ 'descendant with text condition on leaf-list' | '//book/labels[text()="special offer"]' || ["/shops/shop[@id='1']/categories[@code='1']/book"]
'descendant with text condition partial match' | '//book/labels[text()="special"]' || []
- 'descendant with text condition (existing) empty string' | '//book/labels[text()=""]' || ['/shops/shop[@id=1]/categories[@code=1]/book']
- 'descendant with text condition on int leaf-list' | '//book/editions[text()="2000"]' || ['/shops/shop[@id=1]/categories[@code=2]/book']
+ 'descendant with text condition (existing) empty string' | '//book/labels[text()=""]' || ["/shops/shop[@id='1']/categories[@code='1']/book"]
+ 'descendant with text condition on int leaf-list' | '//book/editions[text()="2000"]' || ["/shops/shop[@id='1']/categories[@code='2']/book"]
}
@Sql([CLEAR_DATA, SET_DATA])
@@ -115,10 +115,10 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
}
where: 'the following data is used'
scenario | cpsPath || expectedXPaths
- 'one leaf' | '//author[@FirstName="Joe"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]']
- 'more than one leaf' | '//author[@FirstName="Joe" and @Surname="Bloggs"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]']
- 'leaves reversed in order' | '//author[@Surname="Bloggs" and @FirstName="Joe"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]']
- 'leaf and text condition' | '//author[@FirstName="Joe"]/Surname[text()="Bloggs"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]']
+ 'one leaf' | '//author[@FirstName="Joe"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']", "/shops/shop[@id='1']/categories[@code='2']/book/author[@FirstName='Joe' and @Surname='Smith']"]
+ 'more than one leaf' | '//author[@FirstName="Joe" and @Surname="Bloggs"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"]
+ 'leaves reversed in order' | '//author[@Surname="Bloggs" and @FirstName="Joe"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"]
+ 'leaf and text condition' | '//author[@FirstName="Joe"]/Surname[text()="Bloggs"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"]
}
@Sql([CLEAR_DATA, SET_DATA])
@@ -133,9 +133,9 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
}
where: 'the following data is used'
scenario | cpsPath || expectedXPaths
- 'one partial key leaf' | '//author[@FirstName="Joe"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]']
- 'one non key leaf' | '//author[@title="Dune"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]']
- 'mix of partial key and non key leaf' | '//author[@FirstName="Joe" and @title="Dune"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]']
+ 'one partial key leaf' | '//author[@FirstName="Joe"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']", "/shops/shop[@id='1']/categories[@code='2']/book/author[@FirstName='Joe' and @Surname='Smith']"]
+ 'one non key leaf' | '//author[@title="Dune"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"]
+ 'mix of partial key and non key leaf' | '//author[@FirstName="Joe" and @title="Dune"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"]
}
@Sql([CLEAR_DATA, SET_DATA])
@@ -149,13 +149,13 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
}
where: 'the following data is used'
scenario | cpsPath || expectedXPaths
- 'multiple list-ancestors' | '//book/ancestor::categories' || ['/shops/shop[@id=1]/categories[@code=1]', '/shops/shop[@id=1]/categories[@code=2]']
- 'one ancestor with list value' | '//book/ancestor::categories[@code=1]' || ['/shops/shop[@id=1]/categories[@code=1]']
+ 'multiple list-ancestors' | '//book/ancestor::categories' || ["/shops/shop[@id='1']/categories[@code='1']", "/shops/shop[@id='1']/categories[@code='2']"]
+ 'one ancestor with list value' | '//book/ancestor::categories[@code=1]' || ["/shops/shop[@id='1']/categories[@code='1']"]
'top ancestor' | '//shop[@id=1]/ancestor::shops' || ['/shops']
- 'list with index value in the xpath prefix' | '//categories[@code=1]/book/ancestor::shop[@id=1]' || ['/shops/shop[@id=1]']
- 'ancestor with parent list' | '//book/ancestor::shop[@id=1]/categories[@code=2]' || ['/shops/shop[@id=1]/categories[@code=2]']
- 'ancestor with parent' | '//phonenumbers[@type="mob"]/ancestor::info/contact' || ['/shops/shop[@id=3]/info/contact']
- 'ancestor combined with text condition' | '//book/title[text()="Dune"]/ancestor::shop' || ['/shops/shop[@id=1]']
+ 'list with index value in the xpath prefix' | '//categories[@code=1]/book/ancestor::shop[@id=1]' || ["/shops/shop[@id='1']"]
+ 'ancestor with parent list' | '//book/ancestor::shop[@id=1]/categories[@code=2]' || ["/shops/shop[@id='1']/categories[@code='2']"]
+ 'ancestor with parent' | '//phonenumbers[@type="mob"]/ancestor::info/contact' || ["/shops/shop[@id='3']/info/contact"]
+ 'ancestor combined with text condition' | '//book/title[text()="Dune"]/ancestor::shop' || ["/shops/shop[@id='1']"]
'ancestor with parent that does not exist' | '//book/ancestor::parentDoesNoExist/categories' || []
'ancestor does not exist' | '//book/ancestor::ancestorDoesNotExist' || []
}
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
index ab290051a..6f780fc50 100755
--- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
+++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
@@ -23,11 +23,13 @@ package org.onap.cps.spi.impl
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.collect.ImmutableSet
+import org.onap.cps.cpspath.parser.PathParsingException
import org.onap.cps.spi.CpsDataPersistenceService
import org.onap.cps.spi.entities.FragmentEntity
import org.onap.cps.spi.exceptions.AlreadyDefinedException
import org.onap.cps.spi.exceptions.AnchorNotFoundException
import org.onap.cps.spi.exceptions.CpsAdminException
+import org.onap.cps.spi.exceptions.CpsPathException
import org.onap.cps.spi.exceptions.DataNodeNotFoundException
import org.onap.cps.spi.exceptions.DataspaceNotFoundException
import org.onap.cps.spi.model.DataNode
@@ -150,7 +152,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
thrown(expectedException)
where: 'the following data is used'
scenario | parentXpath | dataNode || expectedException
- 'parent does not exist' | 'unknown' | newDataNode || DataNodeNotFoundException
+ 'parent does not exist' | '/unknown' | newDataNode || DataNodeNotFoundException
'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || AlreadyDefinedException
}
@@ -185,9 +187,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
then: 'a #expectedException is thrown'
thrown(expectedException)
where: 'following parameters were used'
- scenario | parentNodeXpath | listElementXpaths || expectedException
- 'parent node does not exist' | '/unknown' | ['irrelevant'] || DataNodeNotFoundException
- 'already existing fragment' | '/parent-201' | ['/parent-201/child-204[@key="A"]'] || AlreadyDefinedException
+ scenario | parentNodeXpath | listElementXpaths || expectedException
+ 'parent node does not exist' | '/unknown' | ['irrelevant'] || DataNodeNotFoundException
+ 'data fragment already exists' | '/parent-201' | ["/parent-201/child-204[@key='A']"] || AlreadyDefinedException
}
@Sql([CLEAR_DATA, SET_DATA])
@@ -208,6 +210,15 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
}
@Sql([CLEAR_DATA, SET_DATA])
+ def 'Cps Path query with syntax error throws a CPS Path Exception.'() {
+ when: 'trying to execute a query with a syntax (parsing) error'
+ objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS)
+ then: 'exception is thrown'
+ def exceptionThrown = thrown(CpsPathException)
+ assert exceptionThrown.getDetails().contains('failed to parse at line 1 due to extraneous input \'invalid-cps-path\' expecting \'/\'')
+ }
+
+ @Sql([CLEAR_DATA, SET_DATA])
def 'Get data node by xpath with all descendants.'() {
when: 'data node is requested with all descendants'
def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
@@ -235,10 +246,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
then: 'a #expectedException is thrown'
thrown(expectedException)
where: 'the following data is used'
- scenario | dataspaceName | anchorName | xpath || expectedException
- 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
- 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
- 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH' || DataNodeNotFoundException
+ scenario | dataspaceName | anchorName | xpath || expectedException
+ 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | '/not relevant' || DataspaceNotFoundException
+ 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | '/not relevant' || AnchorNotFoundException
+ 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NO XPATH' || DataNodeNotFoundException
}
@Sql([CLEAR_DATA, SET_DATA])
@@ -265,10 +276,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
then: 'a #expectedException is thrown'
thrown(expectedException)
where: 'the following data is used'
- scenario | dataspaceName | anchorName | xpath || expectedException
- 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
- 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
- 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
+ scenario | dataspaceName | anchorName | xpath || expectedException
+ 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | '/not relevant' || DataspaceNotFoundException
+ 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | '/not relevant' || AnchorNotFoundException
+ 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING XPATH' || DataNodeNotFoundException
}
@Sql([CLEAR_DATA, SET_DATA])
@@ -359,10 +370,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
then: 'a #expectedException is thrown'
thrown(expectedException)
where: 'the following data is used'
- scenario | dataspaceName | anchorName | xpath || expectedException
- 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
- 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
- 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
+ scenario | dataspaceName | anchorName | xpath || expectedException
+ 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | '/not relevant' || DataspaceNotFoundException
+ 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | '/not relevant' || AnchorNotFoundException
+ 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING XPATH' || DataNodeNotFoundException
}
@Sql([CLEAR_DATA, SET_DATA])
@@ -468,10 +479,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths)
where: 'following parameters were used'
scenario | targetXpaths | parentFragmentId || expectedRemainingChildXpaths
- 'list element with key' | '/parent-203/child-204[@key="A"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="B"]']
- 'list element with combined keys' | '/parent-202/child-205[@key="A" and @key2="B"]' | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ['/parent-202/child-206[@key="A"]']
+ 'list element with key' | '/parent-203/child-204[@key="A"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ["/parent-203/child-203", "/parent-203/child-204[@key='B']"]
+ 'list element with combined keys' | '/parent-202/child-205[@key="A" and @key2="B"]' | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ["/parent-202/child-206[@key='A']"]
'whole list' | '/parent-203/child-204' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
- 'list element under list element' | '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="A"]', '/parent-203/child-204[@key="B"]']
+ 'list element under list element' | '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ["/parent-203/child-203", "/parent-203/child-204[@key='A']", "/parent-203/child-204[@key='B']"]
}
@Sql([CLEAR_DATA, SET_DATA])
@@ -510,9 +521,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
'child of target' | '/parent-206/child-206' | '/parent-206/child-206' || null
'child data node, parent still exists' | '/parent-206/child-206' | '/parent-206' || '/parent-206'
'list element' | '/parent-206/child-206/grand-child-206[@key="A"]' | '/parent-206/child-206/grand-child-206[@key="A"]' || null
- 'list element, sibling still exists' | '/parent-206/child-206/grand-child-206[@key="A"]' | '/parent-206/child-206/grand-child-206[@key="X"]' || '/parent-206/child-206/grand-child-206[@key="X"]'
+ 'list element, sibling still exists' | '/parent-206/child-206/grand-child-206[@key="A"]' | '/parent-206/child-206/grand-child-206[@key="X"]' || "/parent-206/child-206/grand-child-206[@key='X']"
'container node' | '/parent-206' | '/parent-206' || null
- 'container list node' | '/parent-206[@key="A"]' | '/parent-206[@key="B"]' || '/parent-206[@key="B"]'
+ 'container list node' | '/parent-206[@key="A"]' | '/parent-206[@key="B"]' || "/parent-206[@key='B']"
'root node with xpath /' | '/' | '/' || null
'root node with xpath passed as blank' | '' | '' || null
@@ -523,11 +534,11 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
when: 'data node is deleted'
objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, datanodeXpath)
then: 'a #expectedException is thrown'
- thrown(DataNodeNotFoundException)
+ thrown(expectedException)
where: 'the following parameters were used'
- scenario | datanodeXpath
- 'valid data node, non existent child node' | '/parent-203/child-non-existent'
- 'invalid list element' | '/parent-206/child-206/grand-child-206@key="A"]'
+ scenario | datanodeXpath | expectedException
+ 'valid data node, non existent child node' | '/parent-203/child-non-existent' | DataNodeNotFoundException
+ 'invalid list element' | '/parent-206/child-206/grand-child-206@key="A"]' | PathParsingException
}
@Sql([CLEAR_DATA, SET_DATA])
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
index 7166008ad..b37f471e7 100644
--- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
+++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (c) 2021 Bell Canada.
+ * Modifications Copyright (C) 2021-2022 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,88 +29,111 @@ import org.onap.cps.spi.model.DataNodeBuilder
import org.onap.cps.spi.repository.AnchorRepository
import org.onap.cps.spi.repository.DataspaceRepository
import org.onap.cps.spi.repository.FragmentRepository
+import org.onap.cps.spi.utils.SessionManager
import org.onap.cps.utils.JsonObjectMapper
import spock.lang.Specification
-
class CpsDataPersistenceServiceSpec extends Specification {
def mockDataspaceRepository = Mock(DataspaceRepository)
def mockAnchorRepository = Mock(AnchorRepository)
def mockFragmentRepository = Mock(FragmentRepository)
def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+ def mockSessionManager = Mock(SessionManager)
def objectUnderTest = new CpsDataPersistenceServiceImpl(
- mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper)
+ mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper,mockSessionManager)
def 'Handling of StaleStateException (caused by concurrent updates) during data node tree update.'() {
- def parentXpath = 'parent-01'
+ def parentXpath = '/parent-01'
def myDataspaceName = 'my-dataspace'
def myAnchorName = 'my-anchor'
given: 'data node object'
- def submittedDataNode = new DataNodeBuilder()
- .withXpath(parentXpath)
- .withLeaves(['leaf-name': 'leaf-value'])
- .build()
+ def submittedDataNode = new DataNodeBuilder()
+ .withXpath(parentXpath)
+ .withLeaves(['leaf-name': 'leaf-value'])
+ .build()
and: 'fragment to be updated'
- mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
- def fragmentEntity = new FragmentEntity()
- fragmentEntity.setXpath(parentXpath)
- fragmentEntity.setChildFragments(Collections.emptySet())
- return fragmentEntity
- }
+ mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
+ def fragmentEntity = new FragmentEntity()
+ fragmentEntity.setXpath(parentXpath)
+ fragmentEntity.setChildFragments(Collections.emptySet())
+ return fragmentEntity
+ }
and: 'data node is concurrently updated by another transaction'
- mockFragmentRepository.save(_) >> { throw new StaleStateException("concurrent updates") }
+ mockFragmentRepository.save(_) >> { throw new StaleStateException("concurrent updates") }
when: 'attempt to update data node'
- objectUnderTest.replaceDataNodeTree(myDataspaceName, myAnchorName, submittedDataNode)
+ objectUnderTest.replaceDataNodeTree(myDataspaceName, myAnchorName, submittedDataNode)
then: 'concurrency exception is thrown'
- def concurrencyException = thrown(ConcurrencyException)
- assert concurrencyException.getDetails().contains(myDataspaceName)
- assert concurrencyException.getDetails().contains(myAnchorName)
- assert concurrencyException.getDetails().contains(parentXpath)
+ def concurrencyException = thrown(ConcurrencyException)
+ assert concurrencyException.getDetails().contains(myDataspaceName)
+ assert concurrencyException.getDetails().contains(myAnchorName)
+ assert concurrencyException.getDetails().contains(parentXpath)
}
def 'Retrieving a data node with a property JSON value of #scenario'() {
given: 'a fragment with a property JSON value of #scenario'
- mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
- new FragmentEntity(childFragments: Collections.emptySet(),
- attributes: "{\"some attribute\": ${dataString}}")
- }
+ mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
+ new FragmentEntity(childFragments: Collections.emptySet(),
+ attributes: "{\"some attribute\": ${dataString}}")
+ }
when: 'getting the data node represented by this fragment'
- def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
- 'parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+ def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
+ '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
then: 'the leaf is of the correct value and data type'
- def attributeValue = dataNode.leaves.get('some attribute')
- assert attributeValue == expectedValue
- assert attributeValue.class == expectedDataClass
+ def attributeValue = dataNode.leaves.get('some attribute')
+ assert attributeValue == expectedValue
+ assert attributeValue.class == expectedDataClass
where: 'the following Data Type is passed'
- scenario | dataString || expectedValue | expectedDataClass
- 'just numbers' | '15174' || 15174 | Integer
- 'number with dot' | '15174.32' || 15174.32 | Double
- 'number with 0 value after dot' | '15174.0' || 15174.0 | Double
- 'number with 0 value before dot' | '0.32' || 0.32 | Double
- 'number higher than max int' | '2147483648' || 2147483648 | Long
- 'just text' | '"Test"' || 'Test' | String
- 'number with exponent' | '1.2345e5' || 1.2345e5 | Double
- 'number higher than max int with dot' | '123456789101112.0' || 123456789101112.0 | Double
- 'text and numbers' | '"String = \'1234\'"' || "String = '1234'" | String
- 'number as String' | '"12345"' || '12345' | String
+ scenario | dataString || expectedValue | expectedDataClass
+ 'just numbers' | '15174' || 15174 | Integer
+ 'number with dot' | '15174.32' || 15174.32 | Double
+ 'number with 0 value after dot' | '15174.0' || 15174.0 | Double
+ 'number with 0 value before dot' | '0.32' || 0.32 | Double
+ 'number higher than max int' | '2147483648' || 2147483648 | Long
+ 'just text' | '"Test"' || 'Test' | String
+ 'number with exponent' | '1.2345e5' || 1.2345e5 | Double
+ 'number higher than max int with dot' | '123456789101112.0' || 123456789101112.0 | Double
+ 'text and numbers' | '"String = \'1234\'"' || "String = '1234'" | String
+ 'number as String' | '"12345"' || '12345' | String
}
def 'Retrieving a data node with invalid JSON'() {
given: 'a fragment with invalid JSON'
- mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
- new FragmentEntity(childFragments: Collections.emptySet(), attributes: '{invalid json')
- }
+ mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
+ new FragmentEntity(childFragments: Collections.emptySet(), attributes: '{invalid json')
+ }
when: 'getting the data node represented by this fragment'
- def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
- 'parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+ def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
+ '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
then: 'a data validation exception is thrown'
- thrown(DataValidationException)
+ thrown(DataValidationException)
+ }
+
+ def 'start session'() {
+ when: 'start session'
+ objectUnderTest.startSession()
+ then: 'the session manager method to start session is invoked'
+ 1 * mockSessionManager.startSession()
}
-}
+ def 'close session'() {
+ given: 'session ID'
+ def someSessionId = 'someSessionId'
+ when: 'close session method is called with session ID as parameter'
+ objectUnderTest.closeSession(someSessionId)
+ then: 'the session manager method to close session is invoked with parameter'
+ 1 * mockSessionManager.closeSession(someSessionId)
+ }
+
+ def 'Lock anchor.'(){
+ when: 'lock anchor method is called with anchor entity details'
+ objectUnderTest.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L)
+ then: 'the session manager method to lock anchor is invoked with same parameters'
+ 1 * mockSessionManager.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L)
+ }
+} \ No newline at end of file
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy
new file mode 100644
index 000000000..9b58c8bc3
--- /dev/null
+++ b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy
@@ -0,0 +1,69 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.utils
+
+import org.onap.cps.spi.exceptions.SessionManagerException
+import org.onap.cps.spi.impl.CpsPersistenceSpecBase
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.test.context.jdbc.Sql
+
+class SessionManagerIntegrationSpec extends CpsPersistenceSpecBase{
+
+ final static String SET_DATA = '/data/anchor.sql'
+
+ @Autowired
+ SessionManager objectUnderTest
+
+ def sessionId
+ def shortTimeoutForTesting = 200L
+
+ def setup(){
+ sessionId = objectUnderTest.startSession()
+ }
+
+ def cleanup(){
+ objectUnderTest.closeSession(sessionId)
+ }
+
+ @Sql([CLEAR_DATA, SET_DATA])
+ def 'Lock anchor.'(){
+ when: 'session tries to acquire anchor lock by passing anchor entity details'
+ objectUnderTest.lockAnchor(sessionId, DATASPACE_NAME, ANCHOR_NAME1, shortTimeoutForTesting)
+ then: 'no exception is thrown'
+ noExceptionThrown()
+ }
+
+ @Sql([CLEAR_DATA, SET_DATA])
+ def 'Attempt to lock anchor when another session is holding the lock.'(){
+ given: 'another session that holds an anchor lock'
+ def otherSessionId = objectUnderTest.startSession()
+ objectUnderTest.lockAnchor(otherSessionId,DATASPACE_NAME,ANCHOR_NAME1,shortTimeoutForTesting)
+ when: 'a session tries to acquire the same anchor lock'
+ objectUnderTest.lockAnchor(sessionId,DATASPACE_NAME,ANCHOR_NAME1,shortTimeoutForTesting)
+ then: 'a session manager exception is thrown specifying operation reached timeout'
+ def thrown = thrown(SessionManagerException)
+ thrown.message.contains('Timeout')
+ then: 'when the other session holding the lock is closed, lock can finally be acquired'
+ objectUnderTest.closeSession(otherSessionId)
+ objectUnderTest.lockAnchor(sessionId,DATASPACE_NAME,ANCHOR_NAME1,shortTimeoutForTesting)
+ }
+
+}
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy
new file mode 100644
index 000000000..a2df06ef0
--- /dev/null
+++ b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy
@@ -0,0 +1,99 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.utils
+
+import com.google.common.util.concurrent.TimeLimiter
+import org.hibernate.HibernateException
+import org.hibernate.Transaction
+import org.onap.cps.spi.entities.AnchorEntity
+import org.onap.cps.spi.exceptions.SessionManagerException
+import org.onap.cps.spi.repository.AnchorRepository
+import org.onap.cps.spi.repository.DataspaceRepository
+import org.testcontainers.shaded.com.google.common.util.concurrent.UncheckedExecutionException
+import spock.lang.Specification
+import org.hibernate.Session
+
+import java.util.concurrent.ExecutionException
+
+class SessionManagerSpec extends Specification {
+
+ def spiedTimeLimiterProvider = Spy(TimeLimiterProvider)
+ def mockDataspaceRepository = Mock(DataspaceRepository)
+ def mockAnchorRepository = Mock(AnchorRepository)
+ def mockSession = Mock(Session)
+
+ def objectUnderTest = new SessionManager(spiedTimeLimiterProvider, mockDataspaceRepository, mockAnchorRepository)
+
+ def 'Lock anchor entity with #exceptionDuringTest exception.'(){
+ given: 'a dummy session'
+ objectUnderTest.sessionMap.put('dummySession', mockSession)
+ and: 'the anchor name can be resolved'
+ def mockAnchorEntity = Mock(AnchorEntity)
+ mockAnchorEntity.getId() > 456
+ mockAnchorRepository.getByDataspaceAndName(_, _) >> mockAnchorEntity
+ and: 'timeLimiter throws an #exceptionDuringTest exception'
+ def mockTimeLimiter = Mock(TimeLimiter)
+ spiedTimeLimiterProvider.getTimeLimiter(_) >> mockTimeLimiter
+ mockTimeLimiter.callWithTimeout(*_) >> { throw exceptionDuringTest }
+ when: 'session tries to acquire anchor lock'
+ objectUnderTest.lockAnchor('dummySession', 'some-dataspace','some-anchor', 123L)
+ then: 'a session manager exception is thrown with the expected detail'
+ def thrown = thrown(SessionManagerException)
+ thrown.details.contains(expectedExceptionDetail)
+ where:
+ exceptionDuringTest || expectedExceptionDetail
+ new InterruptedException() || 'interrupted'
+ new ExecutionException() || 'aborted'
+ }
+
+ def 'Close session that does not exist.'() {
+ when: 'attempt to close session that does not exist'
+ objectUnderTest.closeSession('unknown session id')
+ then: 'a session manager exception is thrown with the unknown id in the details'
+ def thrown = thrown(SessionManagerException)
+ assert thrown.details.contains('unknown session id')
+ }
+
+ def 'Hibernate exception while closing session.'() {
+ given: 'a test session with a transaction'
+ objectUnderTest.sessionMap.put('testSessionId', mockSession)
+ mockSession.getTransaction() >> Mock(Transaction)
+ and: 'an hibernate exception when closing that session'
+ def hibernateException = new HibernateException('test')
+ mockSession.close() >> { throw hibernateException }
+ when: 'attempt to close session'
+ objectUnderTest.closeSession('testSessionId')
+ then: 'a session manager exception is thrown with the session id in the details'
+ def thrown = thrown(SessionManagerException)
+ assert thrown.details.contains('testSessionId')
+ and: 'the original exception as cause'
+ assert thrown.cause == hibernateException
+ }
+
+ def 'Attempt to lock anchor entity with session Id that does not exists'(){
+ when: 'attempt to acquire anchor lock with session that does not exists'
+ objectUnderTest.lockAnchor('unknown session id','','',123L)
+ then: 'a session manager exception is thrown with the unknown id in the details'
+ def thrown = thrown(SessionManagerException)
+ thrown.details.contains('unknown session id')
+ }
+
+}
diff --git a/cps-ri/src/test/resources/data/cps-path-query.sql b/cps-ri/src/test/resources/data/cps-path-query.sql
index 8f525df6b..d1a62209e 100644
--- a/cps-ri/src/test/resources/data/cps-path-query.sql
+++ b/cps-ri/src/test/resources/data/cps-path-query.sql
@@ -1,6 +1,6 @@
/*
============LICENSE_START=======================================================
- Copyright (C) 2021 Nordix Foundation.
+ Copyright (C) 2021-2022 Nordix Foundation.
Modifications Copyright (C) 2021 Bell Canada.
================================================================================
Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,25 +30,25 @@ INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES
INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
(1, 1001, 1003, null, '/shops', null),
- (2, 1001, 1003, 1, '/shops/shop[@id=1]', '{"id" : 1, "type" : "bookstore"}'),
- (3, 1001, 1003, 2, '/shops/shop[@id=1]/categories[@code=1]', '{"code" : 1, "type" : "bookstore", "name": "SciFi"}'),
- (4, 1001, 1003, 2, '/shops/shop[@id=1]/categories[@code=2]', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}'),
- (5, 1001, 1003, 3, '/shops/shop[@id=1]/categories[@code=1]/book', '{"price" : 5, "title" : "Dune", "labels" : ["special offer","classics",""]}'),
- (6, 1001, 1003, 4, '/shops/shop[@id=1]/categories[@code=2]/book', '{"price" : 15, "title" : "Chapters", "editions" : [2000,2010,2020]}'),
- (7, 1001, 1003, 5, '/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '{"FirstName" : "Joe", "Surname": "Bloggs","title": "Dune"}'),
- (8, 1001, 1003, 6, '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]', '{"FirstName" : "Joe", "Surname": "Smith","title": "Chapters"}');
+ (2, 1001, 1003, 1, '/shops/shop[@id=''1'']', '{"id" : 1, "type" : "bookstore"}'),
+ (3, 1001, 1003, 2, '/shops/shop[@id=''1'']/categories[@code=''1'']', '{"code" : 1, "type" : "bookstore", "name": "SciFi"}'),
+ (4, 1001, 1003, 2, '/shops/shop[@id=''1'']/categories[@code=''2'']', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}'),
+ (5, 1001, 1003, 3, '/shops/shop[@id=''1'']/categories[@code=''1'']/book', '{"price" : 5, "title" : "Dune", "labels" : ["special offer","classics",""]}'),
+ (6, 1001, 1003, 4, '/shops/shop[@id=''1'']/categories[@code=''2'']/book', '{"price" : 15, "title" : "Chapters", "editions" : [2000,2010,2020]}'),
+ (7, 1001, 1003, 5, '/shops/shop[@id=''1'']/categories[@code=''1'']/book/author[@FirstName=''Joe'' and @Surname=''Bloggs'']', '{"FirstName" : "Joe", "Surname": "Bloggs","title": "Dune"}'),
+ (8, 1001, 1003, 6, '/shops/shop[@id=''1'']/categories[@code=''2'']/book/author[@FirstName=''Joe'' and @Surname=''Smith'']', '{"FirstName" : "Joe", "Surname": "Smith","title": "Chapters"}');
INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
- (9, 1001, 1003, 1, '/shops/shop[@id=2]', '{"type" : "bookstore"}'),
- (10, 1001, 1003, 9, '/shops/shop[@id=2]/categories[@code=1]', '{"code" : 2, "type" : "bookstore", "name": "Kids"}'),
- (11, 1001, 1003, 10, '/shops/shop[@id=2]/categories[@code=2]', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}');
+ (9, 1001, 1003, 1, '/shops/shop[@id=''2'']', '{"type" : "bookstore"}'),
+ (10, 1001, 1003, 9, '/shops/shop[@id=''2'']/categories[@code=''1'']', '{"code" : 2, "type" : "bookstore", "name": "Kids"}'),
+ (11, 1001, 1003, 10, '/shops/shop[@id=''2'']/categories[@code=''2'']', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}');
INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
- (12, 1001, 1003, 1, '/shops/shop[@id=3]', '{"type" : "garden centre"}'),
- (13, 1001, 1003, 12, '/shops/shop[@id=3]/categories[@code=1]', '{"id" : 1, "type" : "garden centre", "name": "indoor plants"}'),
- (14, 1001, 1003, 12, '/shops/shop[@id=3]/categories[@code=2]', '{"id" : 2, "type" : "garden centre", "name": "outdoor plants"}'),
- (16, 1001, 1003, 1, '/shops/shop[@id=3]/info', null),
- (17, 1001, 1003, 1, '/shops/shop[@id=3]/info/contact', null),
- (18, 1001, 1003, 1, '/shops/shop[@id=3]/info/contact/website', '{"address" : "myshop.ie"}'),
- (19, 1001, 1003, 12, '/shops/shop[@id=3]/info/contact/phonenumbers[@type="mob"]', '{"type" : "mob", "number" : "123123456"}'),
- (20, 1001, 1003, 12, '/shops/shop[@id=3]/info/contact/phonenumbers[@type="landline"]', '{"type" : "landline", "number" : "012123456"}');
+ (12, 1001, 1003, 1, '/shops/shop[@id=''3'']', '{"type" : "garden centre"}'),
+ (13, 1001, 1003, 12, '/shops/shop[@id=''3'']/categories[@code=''1'']', '{"id" : 1, "type" : "garden centre", "name": "indoor plants"}'),
+ (14, 1001, 1003, 12, '/shops/shop[@id=''3'']/categories[@code=''2'']', '{"id" : 2, "type" : "garden centre", "name": "outdoor plants"}'),
+ (16, 1001, 1003, 1, '/shops/shop[@id=''3'']/info', null),
+ (17, 1001, 1003, 1, '/shops/shop[@id=''3'']/info/contact', null),
+ (18, 1001, 1003, 1, '/shops/shop[@id=''3'']/info/contact/website', '{"address" : "myshop.ie"}'),
+ (19, 1001, 1003, 12, '/shops/shop[@id=''3'']/info/contact/phonenumbers[@type=''mob'']', '{"type" : "mob", "number" : "123123456"}'),
+ (20, 1001, 1003, 12, '/shops/shop[@id=''3'']/info/contact/phonenumbers[@type=''landline'']', '{"type" : "landline", "number" : "012123456"}');
diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql
index a27bb5fde..410654106 100755
--- a/cps-ri/src/test/resources/data/fragment.sql
+++ b/cps-ri/src/test/resources/data/fragment.sql
@@ -1,6 +1,6 @@
/*
============LICENSE_START=======================================================
- Copyright (C) 2021 Nordix Foundation.
+ Copyright (C) 2021-2022 Nordix Foundation.
Modifications Copyright (C) 2021 Pantheon.tech
Modifications Copyright (C) 2021-2022 Bell Canada.
================================================================================
@@ -21,14 +21,16 @@
*/
INSERT INTO DATASPACE (ID, NAME) VALUES
- (1001, 'DATASPACE-001');
+ (1001, 'DATASPACE-001'),
+ (1002, 'NCMP-Admin');
INSERT INTO SCHEMA_SET (ID, NAME, DATASPACE_ID) VALUES
(2001, 'SCHEMA-SET-001', 1001);
INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES
(3001, 'ANCHOR-001', 1001, 2001),
- (3003, 'ANCHOR-003', 1001, 2001);
+ (3003, 'ANCHOR-003', 1001, 2001),
+ (3004, 'ncmp-dmi-registry', 1002, 2001);
INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH) VALUES
(4001, 1001, 3001, null, '/parent-1'),
@@ -50,21 +52,32 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES)
(4203, 1001, 3003, 4202, '/parent-200/child-201/grand-child', '{"leaf-value": "original"}'),
(4206, 1001, 3003, null, '/parent-201', '{"leaf-value": "original"}'),
(4207, 1001, 3003, 4206, '/parent-201/child-203', '{}'),
- (4208, 1001, 3003, 4206, '/parent-201/child-204[@key="A"]', '{"key": "A"}'),
- (4209, 1001, 3003, 4206, '/parent-201/child-204[@key="B"]', '{"key": "B"}'),
+ (4208, 1001, 3003, 4206, '/parent-201/child-204[@key=''A'']', '{"key": "A"}'),
+ (4209, 1001, 3003, 4206, '/parent-201/child-204[@key=''B'']', '{"key": "B"}'),
(4211, 1001, 3003, null, '/parent-202', '{"leaf-value": "original"}'),
- (4212, 1001, 3003, 4211, '/parent-202/child-205[@key="A" and @key2="B"]', '{"key": "A", "key2": "B"}'),
- (4213, 1001, 3003, 4211, '/parent-202/child-206[@key="A"]', '{"key": "A"}'),
+ (4212, 1001, 3003, 4211, '/parent-202/child-205[@key=''A'' and @key2=''B'']', '{"key": "A", "key2": "B"}'),
+ (4213, 1001, 3003, 4211, '/parent-202/child-206[@key=''A'']', '{"key": "A"}'),
(4214, 1001, 3003, null, '/parent-203', '{"leaf-value": "original"}'),
(4215, 1001, 3003, 4214, '/parent-203/child-203', '{}'),
- (4216, 1001, 3003, 4214, '/parent-203/child-204[@key="A"]', '{"key": "A"}'),
- (4217, 1001, 3003, 4214, '/parent-203/child-204[@key="B"]', '{"key": "B"}'),
- (4218, 1001, 3003, 4217, '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]', '{"key": "B", "key2": "Y"}'),
+ (4216, 1001, 3003, 4214, '/parent-203/child-204[@key=''A'']', '{"key": "A"}'),
+ (4217, 1001, 3003, 4214, '/parent-203/child-204[@key=''B'']', '{"key": "B"}'),
+ (4218, 1001, 3003, 4217, '/parent-203/child-204[@key=''B'']/grand-child-204[@key2=''Y'']', '{"key": "B", "key2": "Y"}'),
(4226, 1001, 3003, null, '/parent-206', '{"leaf-value": "original"}'),
(4227, 1001, 3003, 4226, '/parent-206/child-206', '{}'),
(4228, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206', '{}'),
- (4229, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key="A"]', '{"key": "A"}'),
- (4230, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key="X"]', '{"key": "X"}'),
- (4231, 1001, 3003, null, '/parent-206[@key="A"]', '{"key": "A"}'),
- (4232, 1001, 3003, 4231, '/parent-206[@key="A"]/child-206', '{}'),
- (4233, 1001, 3003, null, '/parent-206[@key="B"]', '{"key": "B"}'); \ No newline at end of file
+ (4229, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key=''A'']', '{"key": "A"}'),
+ (4230, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key=''X'']', '{"key": "X"}'),
+ (4231, 1001, 3003, null, '/parent-206[@key=''A'']', '{"key": "A"}'),
+ (4232, 1001, 3003, 4231, '/parent-206[@key=''A'']/child-206', '{}'),
+ (4233, 1001, 3003, null, '/parent-206[@key=''B'']', '{"key": "B"}');
+
+INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
+ (5000, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo'']', '{"id": "PNFDemo", "dmi-service-name": "http://172.21.235.14:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+ (5001, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo2'']', '{"id": "PNFDemo2", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+ (5002, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo3'']', '{"id": "PNFDemo3", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+ (5003, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo4'']', '{"id": "PNFDemo4", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+ (5004, 1002, 3004, 5000, '/dmi-registry/cm-handles[@id=''PNFDemo'']/public-properties[@name=''Contact'']', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
+ (5005, 1002, 3004, 5001, '/dmi-registry/cm-handles[@id=''PNFDemo2'']/public-properties[@name=''Contact'']', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
+ (5006, 1002, 3004, 5002, '/dmi-registry/cm-handles[@id=''PNFDemo3'']/public-properties[@name=''Contact'']', '{"name": "Contact3", "value": "PNF3@bookstore.com"}'),
+ (5007, 1002, 3004, 5003, '/dmi-registry/cm-handles[@id=''PNFDemo4'']/public-properties[@name=''Contact'']', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
+ (5008, 1002, 3004, 5004, '/dmi-registry/cm-handles[@id=''PNFDemo4'']/public-properties[@name=''Contact2'']', '{"name": "Contact2", "value": "newemailforstore2@bookstore.com"}');
diff --git a/cps-ri/src/test/resources/hibernate.cfg.xml b/cps-ri/src/test/resources/hibernate.cfg.xml
new file mode 100644
index 000000000..fae9275dd
--- /dev/null
+++ b/cps-ri/src/test/resources/hibernate.cfg.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE hibernate-configuration PUBLIC
+ "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
+ "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
+
+<hibernate-configuration>
+ <session-factory>
+ <property name="hibernate.connection.driver_class">org.postgresql.Driver</property>
+ <property name="hibernate.connection.url">${DB_URL}</property>
+ <property name="hibernate.connection.username">${DB_USERNAME}</property>
+ <property name="hibernate.connection.password">${DB_PASSWORD}</property>
+ <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQL82Dialect</property>
+ <property name="show_sql">true</property>
+ <property name="hibernate.hbm2ddl.auto">none</property>
+ </session-factory>
+</hibernate-configuration> \ No newline at end of file
diff --git a/cps-service/pom.xml b/cps-service/pom.xml
index 9c7031e2f..aea122d17 100644
--- a/cps-service/pom.xml
+++ b/cps-service/pom.xml
@@ -28,7 +28,7 @@
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java b/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java
index 44f7f7715..2106f1584 100755
--- a/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java
+++ b/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2020 Nordix Foundation
+ * Copyright (C) 2020-2022 Nordix Foundation
* Modifications Copyright (C) 2020-2022 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
* ================================================================================
@@ -23,9 +23,11 @@
package org.onap.cps.api;
import java.util.Collection;
+import java.util.Set;
import org.onap.cps.spi.exceptions.AlreadyDefinedException;
import org.onap.cps.spi.exceptions.CpsException;
import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
/**
* CPS Admin Service.
@@ -100,4 +102,12 @@ public interface CpsAdminService {
* given module names
*/
Collection<String> queryAnchorNames(String dataspaceName, Collection<String> moduleNames);
+
+ /**
+ * Query and return cm handles that match the given query parameters.
+ *
+ * @param cmHandleQueryParameters the cm handle query parameters
+ * @return collection of cm handle ids
+ */
+ Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters);
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
index cdd417bd8..93c96ec65 100644
--- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
+++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
@@ -174,4 +174,41 @@ public interface CpsDataService {
*/
void updateNodeLeavesAndExistingDescendantLeaves(String dataspaceName, String anchorName, String parentNodeXpath,
String dataNodeUpdatesAsJson, OffsetDateTime observedTimestamp);
+
+ /**
+ * Starts a session which allows use of locks and batch interaction with the persistence service.
+ *
+ * @return Session ID string
+ */
+ String startSession();
+
+ /**
+ * Close session.
+ *
+ * @param sessionId session ID
+ *
+ */
+ void closeSession(String sessionId);
+
+ /**
+ * Lock anchor with default timeout.
+ * To release locks(s), the session holding the lock(s) must be closed.
+ *
+ * @param sessionID session ID
+ * @param dataspaceName dataspace name
+ * @param anchorName anchor name
+ */
+ void lockAnchor(String sessionID, String dataspaceName, String anchorName);
+
+ /**
+ * Lock anchor with custom timeout.
+ * To release locks(s), the session holding the lock(s) must be closed.
+ *
+ * @param sessionID session ID
+ * @param dataspaceName dataspace name
+ * @param anchorName anchor name
+ * @param timeoutInMilliseconds lock attempt timeout in milliseconds
+ */
+ void lockAnchor(String sessionID, String dataspaceName, String anchorName, Long timeoutInMilliseconds);
+
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java
index ecc9bf098..79d6e03d4 100644
--- a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java
+++ b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java
@@ -23,7 +23,6 @@ package org.onap.cps.api;
import java.util.Collection;
import java.util.Map;
-import org.checkerframework.checker.nullness.qual.NonNull;
import org.onap.cps.spi.CascadeDeleteAllowed;
import org.onap.cps.spi.exceptions.DataInUseException;
import org.onap.cps.spi.model.ModuleReference;
@@ -42,8 +41,8 @@ public interface CpsModuleService {
* @param yangResourcesNameToContentMap yang resources (files) as a mep where key is resource name
* and value is content
*/
- void createSchemaSet(@NonNull String dataspaceName, @NonNull String schemaSetName,
- @NonNull Map<String, String> yangResourcesNameToContentMap);
+ void createSchemaSet(String dataspaceName, String schemaSetName,
+ Map<String, String> yangResourcesNameToContentMap);
/**
* Create a schema set from new modules and existing modules.
@@ -52,8 +51,8 @@ public interface CpsModuleService {
* @param newModuleNameToContentMap YANG resources map where key is a module name and value is content
* @param moduleReferences List of YANG resources module references of the modules
*/
- void createSchemaSetFromModules(@NonNull String dataspaceName, @NonNull String schemaSetName,
- @NonNull Map<String, String> newModuleNameToContentMap,
+ void createSchemaSetFromModules(String dataspaceName, String schemaSetName,
+ Map<String, String> newModuleNameToContentMap,
Collection<ModuleReference> moduleReferences);
/**
@@ -63,7 +62,7 @@ public interface CpsModuleService {
* @param schemaSetName schema set name
* @return a SchemaSet
*/
- SchemaSet getSchemaSet(@NonNull String dataspaceName, @NonNull String schemaSetName);
+ SchemaSet getSchemaSet(String dataspaceName, String schemaSetName);
/**
* Deletes Schema Set.
@@ -74,8 +73,8 @@ public interface CpsModuleService {
* @throws DataInUseException if cascadeDeleteAllowed is set to CASCADE_DELETE_PROHIBITED and there
* is associated anchor record exists in database
*/
- void deleteSchemaSet(@NonNull String dataspaceName, @NonNull String schemaSetName,
- @NonNull CascadeDeleteAllowed cascadeDeleteAllowed);
+ void deleteSchemaSet(String dataspaceName, String schemaSetName,
+ CascadeDeleteAllowed cascadeDeleteAllowed);
/**
* Retrieve module references for the given dataspace name.
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java b/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java
index beb0a1540..68ae1ebf0 100644
--- a/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java
+++ b/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2020 Nordix Foundation
+ * Copyright (C) 2020-2022 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,6 @@
package org.onap.cps.api;
import java.util.Collection;
-import org.checkerframework.checker.nullness.qual.NonNull;
import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.model.DataNode;
@@ -40,7 +39,7 @@ public interface CpsQueryService {
* included in the output
* @return a collection of data nodes
*/
- Collection<DataNode> queryDataNodes(@NonNull String dataspaceName, @NonNull String anchorName,
- @NonNull String cpsPath, @NonNull FetchDescendantsOption fetchDescendantsOption);
+ Collection<DataNode> queryDataNodes(String dataspaceName, String anchorName,
+ String cpsPath, FetchDescendantsOption fetchDescendantsOption);
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java
index 1013addbe..762754f9a 100755
--- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2020 Nordix Foundation
+ * Copyright (C) 2020-2022 Nordix Foundation
* Modifications Copyright (C) 2020-2022 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
* ================================================================================
@@ -24,12 +24,15 @@ package org.onap.cps.api.impl;
import java.time.OffsetDateTime;
import java.util.Collection;
+import java.util.Set;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import org.onap.cps.api.CpsAdminService;
import org.onap.cps.api.CpsDataService;
import org.onap.cps.spi.CpsAdminPersistenceService;
import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
+import org.onap.cps.utils.CpsValidator;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@@ -43,43 +46,56 @@ public class CpsAdminServiceImpl implements CpsAdminService {
@Override
public void createDataspace(final String dataspaceName) {
+ CpsValidator.validateNameCharacters(dataspaceName);
cpsAdminPersistenceService.createDataspace(dataspaceName);
}
@Override
public void deleteDataspace(final String dataspaceName) {
+ CpsValidator.validateNameCharacters(dataspaceName);
cpsAdminPersistenceService.deleteDataspace(dataspaceName);
}
@Override
public void createAnchor(final String dataspaceName, final String schemaSetName, final String anchorName) {
+ CpsValidator.validateNameCharacters(dataspaceName, schemaSetName, anchorName);
cpsAdminPersistenceService.createAnchor(dataspaceName, schemaSetName, anchorName);
}
@Override
public Collection<Anchor> getAnchors(final String dataspaceName) {
+ CpsValidator.validateNameCharacters(dataspaceName);
return cpsAdminPersistenceService.getAnchors(dataspaceName);
}
@Override
public Collection<Anchor> getAnchors(final String dataspaceName, final String schemaSetName) {
+ CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
return cpsAdminPersistenceService.getAnchors(dataspaceName, schemaSetName);
}
@Override
public Anchor getAnchor(final String dataspaceName, final String anchorName) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
return cpsAdminPersistenceService.getAnchor(dataspaceName, anchorName);
}
@Override
public void deleteAnchor(final String dataspaceName, final String anchorName) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
cpsDataService.deleteDataNodes(dataspaceName, anchorName, OffsetDateTime.now());
cpsAdminPersistenceService.deleteAnchor(dataspaceName, anchorName);
}
@Override
public Collection<String> queryAnchorNames(final String dataspaceName, final Collection<String> moduleNames) {
+ CpsValidator.validateNameCharacters(dataspaceName);
final Collection<Anchor> anchors = cpsAdminPersistenceService.queryAnchors(dataspaceName, moduleNames);
return anchors.stream().map(Anchor::getName).collect(Collectors.toList());
}
+
+ @Override
+ public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) {
+ return cpsAdminPersistenceService.queryCmHandles(cmHandleQueryParameters);
+ }
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
index aae355d50..2f1067aaf 100755
--- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
@@ -36,6 +36,7 @@ import org.onap.cps.spi.exceptions.DataValidationException;
import org.onap.cps.spi.model.Anchor;
import org.onap.cps.spi.model.DataNode;
import org.onap.cps.spi.model.DataNodeBuilder;
+import org.onap.cps.utils.CpsValidator;
import org.onap.cps.utils.YangUtils;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
@@ -47,6 +48,7 @@ import org.springframework.stereotype.Service;
public class CpsDataServiceImpl implements CpsDataService {
private static final String ROOT_NODE_XPATH = "/";
+ private static final long DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS = 300L;
private final CpsDataPersistenceService cpsDataPersistenceService;
private final CpsAdminService cpsAdminService;
@@ -56,7 +58,8 @@ public class CpsDataServiceImpl implements CpsDataService {
@Override
public void saveData(final String dataspaceName, final String anchorName, final String jsonData,
final OffsetDateTime observedTimestamp) {
- final var dataNode = buildDataNode(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData);
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
+ final DataNode dataNode = buildDataNode(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData);
cpsDataPersistenceService.storeDataNode(dataspaceName, anchorName, dataNode);
processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, ROOT_NODE_XPATH, Operation.CREATE);
}
@@ -64,7 +67,8 @@ public class CpsDataServiceImpl implements CpsDataService {
@Override
public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
final String jsonData, final OffsetDateTime observedTimestamp) {
- final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
+ final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
cpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, parentNodeXpath, dataNode);
processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.CREATE);
}
@@ -72,6 +76,7 @@ public class CpsDataServiceImpl implements CpsDataService {
@Override
public void saveListElements(final String dataspaceName, final String anchorName,
final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Collection<DataNode> listElementDataNodeCollection =
buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
@@ -82,13 +87,15 @@ public class CpsDataServiceImpl implements CpsDataService {
@Override
public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
final FetchDescendantsOption fetchDescendantsOption) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
return cpsDataPersistenceService.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption);
}
@Override
public void updateNodeLeaves(final String dataspaceName, final String anchorName, final String parentNodeXpath,
final String jsonData, final OffsetDateTime observedTimestamp) {
- final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
+ final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
cpsDataPersistenceService
.updateDataLeaves(dataspaceName, anchorName, dataNode.getXpath(), dataNode.getLeaves());
processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE);
@@ -99,6 +106,7 @@ public class CpsDataServiceImpl implements CpsDataService {
final String parentNodeXpath,
final String dataNodeUpdatesAsJson,
final OffsetDateTime observedTimestamp) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Collection<DataNode> dataNodeUpdates =
buildDataNodes(dataspaceName, anchorName,
parentNodeXpath, dataNodeUpdatesAsJson);
@@ -109,9 +117,31 @@ public class CpsDataServiceImpl implements CpsDataService {
}
@Override
+ public String startSession() {
+ return cpsDataPersistenceService.startSession();
+ }
+
+ @Override
+ public void closeSession(final String sessionId) {
+ cpsDataPersistenceService.closeSession(sessionId);
+ }
+
+ @Override
+ public void lockAnchor(final String sessionID, final String dataspaceName, final String anchorName) {
+ lockAnchor(sessionID, dataspaceName, anchorName, DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS);
+ }
+
+ @Override
+ public void lockAnchor(final String sessionID, final String dataspaceName,
+ final String anchorName, final Long timeoutInMilliseconds) {
+ cpsDataPersistenceService.lockAnchor(sessionID, dataspaceName, anchorName, timeoutInMilliseconds);
+ }
+
+ @Override
public void replaceNodeTree(final String dataspaceName, final String anchorName, final String parentNodeXpath,
final String jsonData, final OffsetDateTime observedTimestamp) {
- final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
+ final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
cpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName, dataNode);
processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE);
}
@@ -119,6 +149,7 @@ public class CpsDataServiceImpl implements CpsDataService {
@Override
public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
final String jsonData, final OffsetDateTime observedTimestamp) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Collection<DataNode> newListElements =
buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp);
@@ -127,6 +158,7 @@ public class CpsDataServiceImpl implements CpsDataService {
@Override
public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
final Collection<DataNode> dataNodes, final OffsetDateTime observedTimestamp) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
cpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, dataNodes);
processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE);
}
@@ -134,6 +166,7 @@ public class CpsDataServiceImpl implements CpsDataService {
@Override
public void deleteDataNode(final String dataspaceName, final String anchorName, final String dataNodeXpath,
final OffsetDateTime observedTimestamp) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
cpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath);
processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, dataNodeXpath, Operation.DELETE);
}
@@ -141,7 +174,8 @@ public class CpsDataServiceImpl implements CpsDataService {
@Override
public void deleteDataNodes(final String dataspaceName, final String anchorName,
final OffsetDateTime observedTimestamp) {
- final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
+ final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName);
processDataUpdatedEventAsync(anchor, ROOT_NODE_XPATH, Operation.DELETE, observedTimestamp);
}
@@ -149,6 +183,7 @@ public class CpsDataServiceImpl implements CpsDataService {
@Override
public void deleteListOrListElement(final String dataspaceName, final String anchorName, final String listNodeXpath,
final OffsetDateTime observedTimestamp) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
cpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, listNodeXpath);
processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, listNodeXpath, Operation.DELETE);
}
@@ -156,8 +191,8 @@ public class CpsDataServiceImpl implements CpsDataService {
private DataNode buildDataNode(final String dataspaceName, final String anchorName,
final String parentNodeXpath, final String jsonData) {
- final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
- final var schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName());
+ final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
+ final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName());
if (ROOT_NODE_XPATH.equals(parentNodeXpath)) {
final NormalizedNode<?, ?> normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext);
@@ -176,8 +211,8 @@ public class CpsDataServiceImpl implements CpsDataService {
final String parentNodeXpath,
final String jsonData) {
- final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
- final var schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName());
+ final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
+ final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName());
final NormalizedNode<?, ?> normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath);
final Collection<DataNode> dataNodes = new DataNodeBuilder()
@@ -194,7 +229,7 @@ public class CpsDataServiceImpl implements CpsDataService {
private void processDataUpdatedEventAsync(final String dataspaceName, final String anchorName,
final OffsetDateTime observedTimestamp, final String xpath,
final Operation operation) {
- final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
+ final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
this.processDataUpdatedEventAsync(anchor, xpath, operation, observedTimestamp);
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
index f0e79c60c..db8a81f27 100644
--- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
@@ -33,6 +33,7 @@ import org.onap.cps.spi.exceptions.SchemaSetInUseException;
import org.onap.cps.spi.model.Anchor;
import org.onap.cps.spi.model.ModuleReference;
import org.onap.cps.spi.model.SchemaSet;
+import org.onap.cps.utils.CpsValidator;
import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -48,6 +49,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
@Override
public void createSchemaSet(final String dataspaceName, final String schemaSetName,
final Map<String, String> yangResourcesNameToContentMap) {
+ CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
final var yangTextSchemaSourceSet
= YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap);
cpsModulePersistenceService.storeSchemaSet(dataspaceName, schemaSetName, yangResourcesNameToContentMap);
@@ -58,6 +60,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
public void createSchemaSetFromModules(final String dataspaceName, final String schemaSetName,
final Map<String, String> newModuleNameToContentMap,
final Collection<ModuleReference> moduleReferences) {
+ CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
cpsModulePersistenceService.storeSchemaSetFromModules(dataspaceName, schemaSetName,
newModuleNameToContentMap, moduleReferences);
@@ -65,6 +68,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
@Override
public SchemaSet getSchemaSet(final String dataspaceName, final String schemaSetName) {
+ CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
final var yangTextSchemaSourceSet = yangTextSchemaSourceSetCache
.get(dataspaceName, schemaSetName);
return SchemaSet.builder().name(schemaSetName).dataspaceName(dataspaceName)
@@ -75,6 +79,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
@Transactional
public void deleteSchemaSet(final String dataspaceName, final String schemaSetName,
final CascadeDeleteAllowed cascadeDeleteAllowed) {
+ CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
final Collection<Anchor> anchors = cpsAdminService.getAnchors(dataspaceName, schemaSetName);
if (!anchors.isEmpty() && isCascadeDeleteProhibited(cascadeDeleteAllowed)) {
throw new SchemaSetInUseException(dataspaceName, schemaSetName);
@@ -89,23 +94,25 @@ public class CpsModuleServiceImpl implements CpsModuleService {
@Override
public Collection<ModuleReference> getYangResourceModuleReferences(final String dataspaceName) {
+ CpsValidator.validateNameCharacters(dataspaceName);
return cpsModulePersistenceService.getYangResourceModuleReferences(dataspaceName);
}
@Override
public Collection<ModuleReference> getYangResourcesModuleReferences(final String dataspaceName,
final String anchorName) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
return cpsModulePersistenceService.getYangResourceModuleReferences(dataspaceName, anchorName);
}
- private boolean isCascadeDeleteProhibited(final CascadeDeleteAllowed cascadeDeleteAllowed) {
- return CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED == cascadeDeleteAllowed;
- }
-
@Override
public Collection<ModuleReference> identifyNewModuleReferences(
final Collection<ModuleReference> moduleReferencesToCheck) {
return cpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck);
}
+ private boolean isCascadeDeleteProhibited(final CascadeDeleteAllowed cascadeDeleteAllowed) {
+ return CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED == cascadeDeleteAllowed;
+ }
+
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java
index dd9f160db..c2003d6bf 100644
--- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java
@@ -25,6 +25,7 @@ import org.onap.cps.api.CpsQueryService;
import org.onap.cps.spi.CpsDataPersistenceService;
import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.utils.CpsValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -37,6 +38,7 @@ public class CpsQueryServiceImpl implements CpsQueryService {
@Override
public Collection<DataNode> queryDataNodes(final String dataspaceName, final String anchorName,
final String cpsPath, final FetchDescendantsOption fetchDescendantsOption) {
+ CpsValidator.validateNameCharacters(dataspaceName, anchorName);
return cpsDataPersistenceService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption);
}
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java b/cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java
index 03b52a308..fb881a97b 100644
--- a/cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java
@@ -24,6 +24,7 @@ package org.onap.cps.api.impl;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Map;
import org.onap.cps.spi.CpsModulePersistenceService;
+import org.onap.cps.utils.CpsValidator;
import org.onap.cps.yang.YangTextSchemaSourceSet;
import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
import org.springframework.beans.factory.annotation.Autowired;
@@ -52,6 +53,7 @@ public class YangTextSchemaSourceSetCache {
*/
@Cacheable(key = "#p0.concat('-').concat(#p1)")
public YangTextSchemaSourceSet get(final String dataspaceName, final String schemaSetName) {
+ CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
final Map<String, String> yangResourceNameToContent =
cpsModulePersistenceService.getYangSchemaResources(dataspaceName, schemaSetName);
return YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent);
@@ -69,6 +71,7 @@ public class YangTextSchemaSourceSetCache {
@CanIgnoreReturnValue
public YangTextSchemaSourceSet updateCache(final String dataspaceName, final String schemaSetName,
final YangTextSchemaSourceSet yangTextSchemaSourceSet) {
+ CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
return yangTextSchemaSourceSet;
}
@@ -81,6 +84,7 @@ public class YangTextSchemaSourceSetCache {
*/
@CacheEvict(key = "#p0.concat('-').concat(#p1)")
public void removeFromCache(final String dataspaceName, final String schemaSetName) {
+ CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
// Spring provides implementation for removing object from cache
}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java
index dd4059d88..25167e844 100755
--- a/cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java
+++ b/cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2020 Nordix Foundation.
+ * Copyright (C) 2020-2022 Nordix Foundation.
* Modifications Copyright (C) 2020-2022 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
* ================================================================================
@@ -23,8 +23,10 @@
package org.onap.cps.spi;
import java.util.Collection;
+import java.util.Set;
import org.onap.cps.spi.exceptions.AlreadyDefinedException;
import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
/*
Service for handling CPS admin data.
@@ -99,4 +101,12 @@ public interface CpsAdminPersistenceService {
* @param anchorName anchor name
*/
void deleteAnchor(String dataspaceName, String anchorName);
+
+ /**
+ * Query and return cm handles that match the given query parameters.
+ *
+ * @param cmHandleQueryParameters the cm handle query parameters
+ * @return collection of cm handle ids
+ */
+ Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters);
}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
index fd658861c..43cfffee7 100644
--- a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
+++ b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2020 Nordix Foundation.
+ * Copyright (C) 2020-2022 Nordix Foundation.
* Modifications Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
@@ -148,4 +148,29 @@ public interface CpsDataPersistenceService {
Collection<DataNode> queryDataNodes(String dataspaceName, String anchorName,
String cpsPath, FetchDescendantsOption fetchDescendantsOption);
+ /**
+ * Starts a session which allows use of locks and batch interaction with the persistence service.
+ *
+ * @return Session ID string
+ */
+ String startSession();
+
+ /**
+ * Close session.
+ *
+ * @param sessionId session ID
+ */
+ void closeSession(String sessionId);
+
+ /**
+ * Lock anchor.
+ * To release locks(s), the session holding the lock(s) must be closed.
+ *
+ * @param sessionID session ID
+ * @param dataspaceName dataspace name
+ * @param anchorName anchor name
+ * @param timeoutInMilliseconds lock attempt timeout in milliseconds
+ */
+ void lockAnchor(String sessionID, String dataspaceName, String anchorName, Long timeoutInMilliseconds);
+
}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionManagerException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionManagerException.java
new file mode 100644
index 000000000..4000bfc51
--- /dev/null
+++ b/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionManagerException.java
@@ -0,0 +1,48 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.exceptions;
+
+
+public class SessionManagerException extends CpsException {
+
+ private static final long serialVersionUID = 7957090904519019500L;
+
+ /**
+ * Constructor.
+ *
+ * @param message the error message
+ * @param details the error details
+ * @param cause the cause of the exception
+ */
+ public SessionManagerException(final String message, final String details, final Throwable cause) {
+ super(message, details, cause);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message the error message
+ * @param details the error details
+ */
+ public SessionManagerException(final String message, final String details) {
+ super(message, details);
+ }
+}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionTimeoutException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionTimeoutException.java
new file mode 100644
index 000000000..92b4aa7a8
--- /dev/null
+++ b/cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionTimeoutException.java
@@ -0,0 +1,31 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.exceptions;
+
+@SuppressWarnings("squid:S110") // Team agreed to accept 6 levels of inheritance for CPS Exceptions
+public class SessionTimeoutException extends SessionManagerException {
+
+ private static final long serialVersionUID = -8809577494038691360L;
+
+ public SessionTimeoutException(final String message, final String details, final Throwable cause) {
+ super(message, details, cause);
+ }
+}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java b/cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java
new file mode 100644
index 000000000..ff4e62763
--- /dev/null
+++ b/cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java
@@ -0,0 +1,41 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Collections;
+import java.util.Map;
+import javax.validation.Valid;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+@JsonInclude(Include.NON_NULL)
+public class CmHandleQueryParameters {
+
+ @JsonProperty("publicCmHandleProperties")
+ @Valid
+ private Map<String, String> publicProperties = Collections.emptyMap();
+
+}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java b/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java
index 55e7b9970..43aa06b81 100644
--- a/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java
+++ b/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java
@@ -26,11 +26,13 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@Setter(AccessLevel.PROTECTED)
@Getter
+@EqualsAndHashCode
public class DataNode {
DataNode() { }
diff --git a/cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java b/cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java
new file mode 100644
index 000000000..28b49c966
--- /dev/null
+++ b/cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java
@@ -0,0 +1,62 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.utils;
+
+import com.google.common.collect.Lists;
+import java.util.Collection;
+import java.util.regex.Pattern;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.spi.exceptions.DataValidationException;
+
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class CpsValidator {
+
+ private static final char[] UNSUPPORTED_NAME_CHARACTERS = "!\" #$%&'()*+,./\\:;<=>?@[]^`{|}~".toCharArray();
+ private static final Pattern TOPIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9]([._-](?![._-])|"
+ + "[a-zA-Z0-9]){0,120}[a-zA-Z0-9]$");
+
+ /**
+ * Validate characters in names within cps.
+ * @param names names of data to be validated
+ */
+ public static void validateNameCharacters(final String... names) {
+ for (final String name : names) {
+ final Collection<Character> charactersOfName = Lists.charactersOf(name);
+ for (final char unsupportedCharacter : UNSUPPORTED_NAME_CHARACTERS) {
+ if (charactersOfName.contains(unsupportedCharacter)) {
+ throw new DataValidationException("Name or ID Validation Error.",
+ name + " invalid token encountered at position " + (name.indexOf(unsupportedCharacter) + 1));
+ }
+ }
+ }
+ }
+
+ /**
+ * Validate kafka topic name pattern.
+ * @param topicName name of the topic to be validated
+ */
+ public static boolean validateTopicName(final String topicName) {
+ return topicName != null && TOPIC_NAME_PATTERN.matcher(topicName).matches();
+ }
+}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
index bb122d1ae..33868ccf0 100755
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2020 Nordix Foundation
+ * Copyright (C) 2020-2022 Nordix Foundation
* Modifications Copyright (C) 2020-2022 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
* ================================================================================
@@ -24,7 +24,9 @@ package org.onap.cps.api.impl
import org.onap.cps.api.CpsDataService
import org.onap.cps.spi.CpsAdminPersistenceService
+import org.onap.cps.spi.exceptions.DataValidationException
import org.onap.cps.spi.model.Anchor
+import org.onap.cps.spi.model.CmHandleQueryParameters
import spock.lang.Specification
import java.time.OffsetDateTime
@@ -40,6 +42,15 @@ class CpsAdminServiceImplSpec extends Specification {
1 * mockCpsAdminPersistenceService.createDataspace('someDataspace')
}
+ def 'Create a dataspace with an invalid dataspace name.'() {
+ when: 'create dataspace method is invoked with incorrectly named dataspace'
+ objectUnderTest.createDataspace('Dataspace Name with spaces')
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsAdminPersistenceService.createDataspace(_)
+ }
+
def 'Create anchor method invokes persistence service.'() {
when: 'create anchor method is invoked'
objectUnderTest.createAnchor('someDataspace', 'someSchemaSet', 'someAnchorName')
@@ -47,6 +58,15 @@ class CpsAdminServiceImplSpec extends Specification {
1 * mockCpsAdminPersistenceService.createAnchor('someDataspace', 'someSchemaSet', 'someAnchorName')
}
+ def 'Create an anchor with an invalid anchor name.'() {
+ when: 'create anchor method is invoked with incorrectly named dataspace'
+ objectUnderTest.createAnchor('someDataspace', 'someSchemaSet', 'Anchor Name With Spaces')
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsAdminPersistenceService.createAnchor(_, _, _)
+ }
+
def 'Retrieve all anchors for dataspace.'() {
given: 'that anchor is associated with the dataspace'
def anchors = [new Anchor()]
@@ -55,6 +75,15 @@ class CpsAdminServiceImplSpec extends Specification {
objectUnderTest.getAnchors('someDataspace') == anchors
}
+ def 'Retrieve all anchors with an invalid dataspace name.'() {
+ when: 'get anchors is invoked with an invalid dataspace name'
+ objectUnderTest.getAnchors('Dataspace name with spaces')
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'cps admin persistence get anchors is not invoked'
+ 0 * mockCpsAdminPersistenceService.getAnchors(_)
+ }
+
def 'Retrieve all anchors for schema-set.'() {
given: 'that anchor is associated with the dataspace and schemaset'
def anchors = [new Anchor()]
@@ -62,6 +91,20 @@ class CpsAdminServiceImplSpec extends Specification {
expect: 'the collection provided by persistence service is returned as result'
objectUnderTest.getAnchors('someDataspace', 'someSchemaSet') == anchors
}
+ def 'Retrieve all anchors for schema-set with invalid #scenario.'() {
+ when: 'the collection provided by persistence service is returned as result'
+ objectUnderTest.getAnchors(dataspaceName, schemaSetName)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'cps admin persistence get anchors is not invoked'
+ 0 * mockCpsAdminPersistenceService.getAnchors(_, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | schemaSetName
+ 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName'
+ 'schema set name' | 'dataspaceName' | 'schema set name with spaces'
+ 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces'
+ }
+
def 'Retrieve anchor for dataspace and provided anchor name.'() {
given: 'that anchor name is associated with the dataspace'
@@ -71,6 +114,20 @@ class CpsAdminServiceImplSpec extends Specification {
assert objectUnderTest.getAnchor('someDataspace','someAnchor') == anchor
}
+ def 'Retrieve anchor with invalid #scenario.'() {
+ when: 'get anchors is invoked with an invalid dataspace name'
+ objectUnderTest.getAnchor(dataspaceName, anchorName)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'cps admin persistence get anchor is not invoked'
+ 0 * mockCpsAdminPersistenceService.getAnchor(_, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Delete anchor.'() {
when: 'delete anchor is invoked'
objectUnderTest.deleteAnchor('someDataspace','someAnchor')
@@ -80,6 +137,22 @@ class CpsAdminServiceImplSpec extends Specification {
1 * mockCpsAdminPersistenceService.deleteAnchor('someDataspace','someAnchor')
}
+ def 'Delete anchor with invalid #scenario.'() {
+ when: 'delete anchor is invoked'
+ objectUnderTest.deleteAnchor(dataspaceName, anchorName)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'delete data nodes is invoked on the data service with expected parameters'
+ 0 * mockCpsDataService.deleteDataNodes(_,_, _ as OffsetDateTime )
+ and: 'the persistence service method is invoked with same parameters to delete anchor'
+ 0 * mockCpsAdminPersistenceService.deleteAnchor(_,_)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Query all anchor identifiers for a dataspace and module names.'() {
given: 'the persistence service is invoked with the expected parameters and returns a list of anchors'
mockCpsAdminPersistenceService.queryAnchors('some-dataspace-name', ['some-module-name']) >> [new Anchor(name:'some-anchor-identifier')]
@@ -88,6 +161,15 @@ class CpsAdminServiceImplSpec extends Specification {
}
+ def 'Query all anchor identifiers for a dataspace and module names with an invalid dataspace name.'() {
+ when: 'delete anchor is invoked'
+ objectUnderTest.queryAnchorNames('some dataspace name', _ as Collection<String>)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'delete data nodes is not invoked'
+ 0 * mockCpsAdminPersistenceService.queryAnchors(_, _)
+ }
+
def 'Delete dataspace.'() {
when: 'delete dataspace is invoked'
objectUnderTest.deleteDataspace('someDataspace')
@@ -95,4 +177,22 @@ class CpsAdminServiceImplSpec extends Specification {
1 * mockCpsAdminPersistenceService.deleteDataspace('someDataspace')
}
+ def 'Query CM Handles.'() {
+ given: 'a cm handle query'
+ def cmHandleQueryParameters = new CmHandleQueryParameters()
+ when: 'query cm handles is invoked'
+ objectUnderTest.queryCmHandles(cmHandleQueryParameters)
+ then: 'associated persistence service method is invoked with correct parameter'
+ 1 * mockCpsAdminPersistenceService.queryCmHandles(cmHandleQueryParameters)
+ }
+
+ def 'Delete dataspace with invalid dataspace id.'() {
+ when: 'delete dataspace is invoked'
+ objectUnderTest.deleteDataspace('some dataspace name')
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'associated persistence service method is not invoked'
+ 0 * mockCpsAdminPersistenceService.deleteDataspace(_)
+ }
+
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
index 785788be9..8b9d54529 100644
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
@@ -30,6 +30,7 @@ import org.onap.cps.spi.CpsDataPersistenceService
import org.onap.cps.spi.FetchDescendantsOption
import org.onap.cps.spi.exceptions.DataValidationException
import org.onap.cps.spi.model.Anchor
+import org.onap.cps.spi.model.DataNode
import org.onap.cps.spi.model.DataNodeBuilder
import org.onap.cps.yang.YangTextSchemaSourceSet
import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
@@ -50,9 +51,9 @@ class CpsDataServiceImplSpec extends Specification {
mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
}
- def dataspaceName = 'some dataspace'
- def anchorName = 'some anchor'
- def schemaSetName = 'some schema set'
+ def dataspaceName = 'some-dataspace'
+ def anchorName = 'some-anchor'
+ def schemaSetName = 'some-schema-set'
def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build()
def observedTimestamp = OffsetDateTime.now()
@@ -69,6 +70,22 @@ class CpsDataServiceImplSpec extends Specification {
1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/', Operation.CREATE)
}
+ def 'Saving json data with invalid #scenario.'() {
+ when: 'save data method is invoked with invalid #scenario'
+ objectUnderTest.saveData(dataspaceName, anchorName, _ as String, observedTimestamp)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsDataPersistenceService.storeDataNode(_, _, _)
+ and: 'data updated event is not sent to notification service'
+ 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Saving child data fragment under existing node.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
@@ -82,6 +99,22 @@ class CpsDataServiceImplSpec extends Specification {
1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/test-tree', Operation.CREATE)
}
+ def 'Saving child data fragment under existing node with invalid #scenario.'() {
+ when: 'save data method is invoked with test-tree and an invalid #scenario'
+ objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', _ as String, observedTimestamp)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsDataPersistenceService.addChildDataNode(_, _, _,_)
+ and: 'data updated event is not sent to notification service'
+ 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Saving list element data fragment under existing node.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
@@ -112,6 +145,20 @@ class CpsDataServiceImplSpec extends Specification {
thrown(DataValidationException)
}
+ def 'Saving list element data fragment with invalid #scenario.'() {
+ when: 'save data method is invoked with an invalid #scenario'
+ objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', _ as String, observedTimestamp)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'add list elements persistence method is not invoked'
+ 0 * mockCpsDataPersistenceService.addListElements(_, _, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Get data node with option #fetchDescendantsOption.'() {
def xpath = '/xpath'
def dataNode = new DataNodeBuilder().withXpath(xpath).build()
@@ -123,6 +170,20 @@ class CpsDataServiceImplSpec extends Specification {
fetchDescendantsOption << FetchDescendantsOption.values()
}
+ def 'Get data node with option invalid #scenario.'() {
+ when: 'get data node is invoked with #scenario'
+ objectUnderTest.getDataNode(dataspaceName, anchorName, '/test-tree', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'get data node persistence service is not invoked'
+ 0 * mockCpsDataPersistenceService.getDataNode(_, _, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Update data node leaves: #scenario.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
@@ -138,6 +199,22 @@ class CpsDataServiceImplSpec extends Specification {
'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']' | ['name': 'Name']
}
+ def 'Update data node with invalid #scenario.'() {
+ when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
+ objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/', '{"test-tree": {"branch": []}}', observedTimestamp)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsDataPersistenceService.updateDataLeaves(_, _, _, _)
+ and: 'data updated event is not sent to notification service'
+ 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Update list-element data node with : #scenario.'() {
given: 'schema set for given anchor and dataspace references bookstore model'
setupSchemaSetMocks('bookstore.yang')
@@ -167,6 +244,24 @@ class CpsDataServiceImplSpec extends Specification {
1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/bookstore', Operation.UPDATE)
}
+ def 'Update Bookstore node leaves with invalid #scenario' () {
+ when: 'update data method is invoked with an invalid #scenario'
+ objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName,
+ '/bookstore', _ as String, observedTimestamp)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsDataPersistenceService.updateDataLeaves(_, _, _, _)
+ and: 'the data updated event is not sent to the notification service'
+ 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
+
def 'Replace data node: #scenario.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
@@ -183,6 +278,22 @@ class CpsDataServiceImplSpec extends Specification {
'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']'
}
+ def 'Replace data node with invalid #scenario.'() {
+ when: 'replace data method is invoked with invalid #scenario'
+ objectUnderTest.replaceNodeTree(dataspaceName, anchorName, '/', _ as String, observedTimestamp)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsDataPersistenceService.replaceDataNodeTree(_, _,_)
+ and: 'data updated event is not sent to notification service'
+ 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Replace list content data fragment under parent node.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
@@ -213,6 +324,22 @@ class CpsDataServiceImplSpec extends Specification {
thrown(DataValidationException)
}
+ def 'Replace whole list content with an invalid #scenario.'() {
+ when: 'replace list data method is invoked with invalid #scenario'
+ objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', _ as Collection<DataNode>, observedTimestamp)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsDataPersistenceService.replaceListContent(_, _,_)
+ and: 'data updated event is not sent to notification service'
+ 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Delete list element under existing node.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
@@ -224,6 +351,23 @@ class CpsDataServiceImplSpec extends Specification {
1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/test-tree/branch', Operation.DELETE)
}
+
+ def 'Delete list element with an invalid #scenario.'() {
+ when: 'delete list data method is invoked with with invalid #scenario'
+ objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsDataPersistenceService.deleteListDataNode(_, _, _)
+ and: 'data updated event is not sent to notification service'
+ 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Delete data node under anchor and dataspace.'() {
given: 'schema set for given anchor and dataspace references test tree model'
setupSchemaSetMocks('test-tree.yang')
@@ -235,6 +379,22 @@ class CpsDataServiceImplSpec extends Specification {
1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/data-node', Operation.DELETE)
}
+ def 'Delete data node with an invalid #scenario.'() {
+ when: 'delete data node method is invoked with invalid #scenario'
+ objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsDataPersistenceService.deleteDataNode(_, _, _)
+ and: 'data updated event is not sent to notification service'
+ 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Delete all data nodes for a given anchor and dataspace.'() {
given: 'schema set for given anchor and dataspace references test tree model'
setupSchemaSetMocks('test-tree.yang')
@@ -254,4 +414,37 @@ class CpsDataServiceImplSpec extends Specification {
def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
}
+
+ def 'start session'() {
+ when: 'start session method is called'
+ objectUnderTest.startSession()
+ then: 'the persistence service method to start session is invoked'
+ 1 * mockCpsDataPersistenceService.startSession()
+ }
+
+ def 'close session'(){
+ given: 'session Id from calling the start session method'
+ def sessionId = objectUnderTest.startSession()
+ when: 'close session method is called'
+ objectUnderTest.closeSession(sessionId)
+ then: 'the persistence service method to close session is invoked'
+ 1 * mockCpsDataPersistenceService.closeSession(sessionId)
+ }
+
+ def 'lock anchor with no timeout parameter'(){
+ when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
+ objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
+ then: 'the persistence service method to lock anchor is invoked with default timeout'
+ 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
+ 'some-anchorName', 300L)
+ }
+
+ def 'lock anchor with timeout parameter'(){
+ when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
+ objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName',
+ 'some-anchorName', 250L)
+ then: 'the persistence service method to lock anchor is invoked with the given timeout'
+ 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
+ 'some-anchorName', 250L)
+ }
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
index bae06bb9e..95d731478 100644
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
@@ -24,7 +24,9 @@ package org.onap.cps.api.impl
import org.onap.cps.TestUtils
import org.onap.cps.api.CpsAdminService
+import org.onap.cps.spi.CascadeDeleteAllowed
import org.onap.cps.spi.CpsModulePersistenceService
+import org.onap.cps.spi.exceptions.DataValidationException
import org.onap.cps.spi.exceptions.ModelValidationException
import org.onap.cps.spi.exceptions.SchemaSetInUseException
import org.onap.cps.spi.model.Anchor
@@ -51,6 +53,20 @@ class CpsModuleServiceImplSpec extends Specification {
1 * mockCpsModulePersistenceService.storeSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap)
}
+ def 'Create a schema set with an invalid #scenario.'() {
+ when: 'create dataspace method is invoked with incorrectly named dataspace'
+ objectUnderTest.createSchemaSet(dataspaceName, schemaSetName, _ as Map<String, String>)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsModulePersistenceService.storeSchemaSet(_, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | schemaSetName
+ 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName'
+ 'schema set name name' | 'dataspaceName' | 'schema set name with spaces'
+ 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces'
+ }
+
def 'Create schema set from new modules and existing modules.'() {
given: 'a list of existing modules module reference'
def moduleReferenceForExistingModule = new ModuleReference("test", "2021-10-12","test.org")
@@ -61,6 +77,20 @@ class CpsModuleServiceImplSpec extends Specification {
1 * mockCpsModulePersistenceService.storeSchemaSetFromModules("someDataspaceName", "someSchemaSetName", [newModule: "newContent"], listOfExistingModulesModuleReference)
}
+ def 'Create schema set from new modules and existing modules with invalid #scenario.'() {
+ when: 'create dataspace method is invoked with incorrectly named dataspace'
+ objectUnderTest.createSchemaSetFromModules(dataspaceName, schemaSetName, _ as Map<String, String>, _ as Collection<ModuleReference>)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsModulePersistenceService.storeSchemaSetFromModules(_, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | schemaSetName
+ 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName'
+ 'schema set name name' | 'dataspaceName' | 'schema set name with spaces'
+ 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces'
+ }
+
def 'Create schema set from invalid resources'() {
given: 'Invalid yang resource as name-to-content map'
def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('invalid.yang')
@@ -83,6 +113,20 @@ class CpsModuleServiceImplSpec extends Specification {
result.getModuleReferences().contains(new ModuleReference('stores', '2020-09-15', 'org:onap:ccsdk:sample'))
}
+ def 'Get a schema set with an invalid #scenario'() {
+ when: 'create dataspace method is invoked with incorrectly named dataspace'
+ objectUnderTest.getSchemaSet(dataspaceName, schemaSetName)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the yang resource cache is not invoked'
+ 0 * mockYangTextSchemaSourceSetCache.get(_, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | schemaSetName
+ 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName'
+ 'schema set name' | 'dataspaceName' | 'schema set name with spaces'
+ 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces'
+ }
+
def 'Delete schema-set when cascade is allowed.'() {
given: '#numberOfAnchors anchors are associated with schemaset'
def associatedAnchors = createAnchors(numberOfAnchors)
@@ -125,6 +169,26 @@ class CpsModuleServiceImplSpec extends Specification {
thrown(SchemaSetInUseException)
}
+ def 'Delete a schema set with an invalid #scenario.'() {
+ when: 'create dataspace method is invoked with incorrectly named dataspace'
+ objectUnderTest.deleteSchemaSet(dataspaceName, schemaSetName, CASCADE_DELETE_ALLOWED)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'anchor deletion is called 0 times'
+ 0 * mockCpsAdminService.deleteAnchor(_, _)
+ and: 'the delete schema set persistence service method is not invoked'
+ 0 * mockCpsModulePersistenceService.deleteSchemaSet(_, _, _)
+ and: 'schema set will be removed from the cache is not invoked'
+ 0 * mockYangTextSchemaSourceSetCache.removeFromCache(_, _)
+ and: 'orphan yang resources are deleted is not invoked'
+ 0 * mockCpsModulePersistenceService.deleteUnusedYangResourceModules()
+ where: 'the following parameters are used'
+ scenario | dataspaceName | schemaSetName
+ 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName'
+ 'schema set name name' | 'dataspaceName' | 'schema set name with spaces'
+ 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces'
+ }
+
def createAnchors(int anchorCount) {
def anchors = []
(0..<anchorCount).each { anchors.add(new Anchor("my-anchor-$it", 'my-dataspace', 'my-schemaset')) }
@@ -139,6 +203,15 @@ class CpsModuleServiceImplSpec extends Specification {
objectUnderTest.getYangResourceModuleReferences('someDataspaceName') == moduleReferences
}
+ def 'Get all yang resources module references given an invalid dataspace name.'() {
+ when: 'the get yang resources module references method is invoked with an invalid dataspace name'
+ objectUnderTest.getYangResourceModuleReferences('dataspace name with spaces')
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsModulePersistenceService.getYangResourceModuleReferences(_)
+ }
+
def 'Get all yang resources module references for the given dataspace name and anchor name.'() {
given: 'the module store service service returns a list module references'
@@ -148,6 +221,20 @@ class CpsModuleServiceImplSpec extends Specification {
objectUnderTest.getYangResourcesModuleReferences('someDataspaceName', 'someAnchorName') == moduleReferences
}
+ def 'Get all yang resources module references given an invalid #scenario.'() {
+ when: 'the get yang resources module references method is invoked with invalid #scenario'
+ objectUnderTest.getYangResourcesModuleReferences(dataspaceName, anchorName)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service method is not invoked'
+ 0 * mockCpsModulePersistenceService.getYangResourceModuleReferences(_, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
def 'Identifying new module references'(){
given: 'module references from cm handle'
def moduleReferencesToCheck = [new ModuleReference('some-module', 'some-revision')]
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy
index 4878f4c11..55a252c27 100644
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy
@@ -22,6 +22,7 @@ package org.onap.cps.api.impl
import org.onap.cps.spi.CpsDataPersistenceService
import org.onap.cps.spi.FetchDescendantsOption
+import org.onap.cps.spi.exceptions.DataValidationException
import spock.lang.Specification
class CpsQueryServiceImplSpec extends Specification {
@@ -35,8 +36,8 @@ class CpsQueryServiceImplSpec extends Specification {
def 'Query data nodes by cps path with #fetchDescendantsOption.'() {
given: 'a dataspace name, an anchor name and a cps path'
- def dataspaceName = 'some dataspace'
- def anchorName = 'some anchor'
+ def dataspaceName = 'some-dataspace'
+ def anchorName = 'some-anchor'
def cpsPath = '/cps-path'
when: 'queryDataNodes is invoked'
objectUnderTest.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption)
@@ -45,4 +46,19 @@ class CpsQueryServiceImplSpec extends Specification {
where: 'all fetch descendants options are supported'
fetchDescendantsOption << FetchDescendantsOption.values()
}
+
+ def 'Query data nodes by cps path with invalid #scenario.'() {
+ when: 'queryDataNodes is invoked'
+ objectUnderTest.queryDataNodes(dataspaceName, anchorName, '/cps-path', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+ then: 'a data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'the persistence service is not invoked'
+ 0 * mockCpsDataPersistenceService.queryDataNodes(_, _, _, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | anchorName
+ 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
+ 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
+ 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
+ }
+
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/YangTextSchemaSourceSetCacheSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/YangTextSchemaSourceSetCacheSpec.groovy
index 860b7399d..06c675a25 100644
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/YangTextSchemaSourceSetCacheSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/YangTextSchemaSourceSetCacheSpec.groovy
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2022 Bell Canada
+ * Modifications Copyright (C) 2022 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,6 +23,7 @@ package org.onap.cps.api.impl
import org.onap.cps.TestUtils
import org.onap.cps.spi.CpsModulePersistenceService
+import org.onap.cps.spi.exceptions.DataValidationException
import org.onap.cps.yang.YangTextSchemaSourceSet
import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
import org.spockframework.spring.SpringBean
@@ -88,6 +90,20 @@ class YangTextSchemaSourceSetCacheSpec extends Specification {
0 * mockModuleStoreService.getYangSchemaResources(_, _)
}
+ def 'Cache Hit: with invalid #scenario'() {
+ when: 'schema-set information is asked'
+ objectUnderTest.get(dataspaceName, schemaSetName)
+ then: 'an data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'module persistence is not invoked'
+ 0 * mockModuleStoreService.getYangSchemaResources(_, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | schemaSetName
+ 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName'
+ 'schema set name' | 'dataspaceName' | 'schema set name with spaces'
+ 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces'
+ }
+
def 'Cache Update: when no data exist in the cache'() {
given: 'a schema set exists'
def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang')
@@ -99,7 +115,24 @@ class YangTextSchemaSourceSetCacheSpec extends Specification {
cachedValue.getModuleReferences() == yangTextSchemaSourceSet.getModuleReferences()
}
- def 'Cache Evict: remove when exist'() {
+ def 'Cache Update: with invalid #scenario'() {
+ given: 'a schema set exists'
+ def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang')
+ def yangTextSchemaSourceSet = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap)
+ when: 'schema-set information is asked'
+ objectUnderTest.updateCache(dataspaceName, schemaSetName, yangTextSchemaSourceSet)
+ then: 'an data validation exception is thrown'
+ thrown(DataValidationException)
+ and: 'module persistence is not invoked'
+ 0 * mockModuleStoreService.getYangSchemaResources(_, _)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | schemaSetName
+ 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName'
+ 'schema set name' | 'dataspaceName' | 'schema set name with spaces'
+ 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces'
+ }
+
+ def 'Cache Evict:with invalid #scenario'() {
given: 'a schema set exists in cache'
def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang')
def yangTextSchemaSourceSet = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap)
@@ -112,6 +145,18 @@ class YangTextSchemaSourceSetCacheSpec extends Specification {
assert getCachedValue('my-dataspace', 'my-schemaset') == null
}
+ def 'Cache Evict: remove when exist'() {
+ when: 'cache is evicted for schemaset'
+ objectUnderTest.removeFromCache(dataspaceName, schemaSetName)
+ then: 'an data validation exception is thrown'
+ thrown(DataValidationException)
+ where: 'the following parameters are used'
+ scenario | dataspaceName | schemaSetName
+ 'dataspace name' | 'dataspace names with spaces' | 'schemaSetName'
+ 'schema set name' | 'dataspaceName' | 'schema set name with spaces'
+ 'dataspace and schema set name' | 'dataspace name with spaces' | 'schema set name with spaces'
+ }
+
def 'Cache Evict: remove when does not exist'() {
given: 'cache is empty'
yangResourceCacheImpl.clear()
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy
new file mode 100644
index 000000000..ce728ef1c
--- /dev/null
+++ b/cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy
@@ -0,0 +1,62 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.utils
+
+import org.onap.cps.spi.exceptions.DataValidationException
+import spock.lang.Specification
+
+class CpsValidatorSpec extends Specification {
+
+
+ def 'Validating a valid string.'() {
+ when: 'the string is validated using a valid name'
+ CpsValidator.validateNameCharacters('name-with-no-spaces')
+ then: 'no exception is thrown'
+ noExceptionThrown()
+ }
+
+ def 'Validating an invalid string.'() {
+ when: 'the string is validated using an invalid name'
+ CpsValidator.validateNameCharacters(name)
+ then: 'a data validation exception is thrown'
+ def exceptionThrown = thrown(DataValidationException)
+ and: 'the error was encountered at the following index in #scenario'
+ assert exceptionThrown.getDetails().contains(expectedErrorMessage)
+ where: 'the following names are used'
+ scenario | name || expectedErrorMessage
+ 'position 5' | 'name with spaces' || 'name with spaces invalid token encountered at position 5'
+ 'position 9' | 'nameWith Space' || 'nameWith Space invalid token encountered at position 9'
+ }
+
+ def 'Validating topic names.'() {
+ when: 'the topic name is validated'
+ def isValidTopicName = CpsValidator.validateTopicName(topicName)
+ then: 'boolean response will be returned for #scenario'
+ assert isValidTopicName == booleanResponse
+ where: 'the following names are used'
+ scenario | topicName || booleanResponse
+ 'valid topic' | 'my-topic-name' || true
+ 'empty topic' | '' || false
+ 'blank topic' | ' ' || false
+ 'null topic' | null || false
+ 'invalid non empty topic' | '1_5_*_#' || false
+ }
+}
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangTextSchemaSourceSetSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy
index b6250612e..236221aca 100644
--- a/cps-service/src/test/groovy/org/onap/cps/utils/YangTextSchemaSourceSetSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy
@@ -1,7 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2020-2021 Pantheon.tech
- * Modifications Copyright (C) 2020-2021 Nordix Foundation
+ * Modifications Copyright (C) 2020-2022 Nordix Foundation
* Modifications Copyright (C) 2021 Bell Canada.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,22 +20,24 @@
* ============LICENSE_END=========================================================
*/
-package org.onap.cps.utils
+package org.onap.cps.yang
+
import org.onap.cps.TestUtils
import org.onap.cps.spi.exceptions.ModelValidationException
-import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
import org.opendaylight.yangtools.yang.common.Revision
import spock.lang.Specification
-class YangTextSchemaSourceSetSpec extends Specification {
+class YangTextSchemaSourceSetBuilderSpec extends Specification {
def 'Building a valid YangTextSchemaSourceSet using #filenameCase filename.'() {
given: 'a yang model (file)'
def yangResourceNameToContent = [filename: TestUtils.getResourceFileContent('bookstore.yang')]
when: 'the content is parsed'
def result = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
- then: 'the result contains 1 module of the correct name and revision'
+ then: 'it can be validated successfully'
+ YangTextSchemaSourceSetBuilder.validate(yangResourceNameToContent)
+ and: 'the result contains 1 module of the correct name and revision'
result.modules.size() == 1
def optionalModule = result.findModule('stores', Revision.of('2020-09-15'))
optionalModule.isPresent()
diff --git a/csit/data/cmHandleRegistration.json b/csit/data/cmHandleRegistration.json
deleted file mode 100644
index 0133148fd..000000000
--- a/csit/data/cmHandleRegistration.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "cmHandles": [
- "PNFDemo"
- ]
-} \ No newline at end of file
diff --git a/csit/plans/cps/test.properties b/csit/plans/cps/test.properties
index 53b7d40d0..47bb43b0a 100644
--- a/csit/plans/cps/test.properties
+++ b/csit/plans/cps/test.properties
@@ -23,4 +23,4 @@ DMI_SERVICE_URL=http://$LOCAL_IP:$DMI_PORT
DOCKER_REPO=nexus3.onap.org:10003
CPS_VERSION=latest
-DMI_VERSION=1.1.0-SNAPSHOT-latest \ No newline at end of file
+DMI_VERSION=1.2.0-SNAPSHOT-latest \ No newline at end of file
diff --git a/csit/plans/cps/testplan.txt b/csit/plans/cps/testplan.txt
index 8069bb72e..d4615e701 100644
--- a/csit/plans/cps/testplan.txt
+++ b/csit/plans/cps/testplan.txt
@@ -1,5 +1,5 @@
# ============LICENSE_START=======================================================
-# Copyright (C) 2021 Nordix Foundation
+# Copyright (C) 2021-2022 Nordix Foundation
# ================================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,8 +17,8 @@
# Test suites are relative paths under csit/tests/.
# Place the suites in run order.
actuator
-cps-model-sync
-ncmp-passthrough
cps-admin
cps-data
-
+cps-model-sync
+ncmp-passthrough
+public-properties-query \ No newline at end of file
diff --git a/csit/tests/cps-model-sync/cps-model-sync.robot b/csit/tests/cps-model-sync/cps-model-sync.robot
index dfad94861..ea082b5a8 100644
--- a/csit/tests/cps-model-sync/cps-model-sync.robot
+++ b/csit/tests/cps-model-sync/cps-model-sync.robot
@@ -34,7 +34,7 @@ ${auth} Basic Y3BzdXNlcjpjcHNyMGNrcyE=
${ncmpInventoryBasePath} /ncmpInventory
${ncmpBasePath} /ncmp
${dmiUrl} http://${DMI_HOST}:${DMI_PORT}
-${jsonDataCreate} {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","createdCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Sci-Fi Book"},"publicCmHandleProperties":{"Contact":"storeemail@bookstore.com"}}]}
+${jsonDataCreate} {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","createdCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Sci-Fi Book"},"publicCmHandleProperties":{"Contact":"storeemail@bookstore.com", "Contact2":"storeemail2@bookstore.com"}}]}
${jsonDataUpdate} {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","updatedCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Romance Book"},"publicCmHandleProperties":{"Contact":"newemailforstore@bookstore.com"}}]}
*** Test Cases ***
@@ -42,7 +42,7 @@ Register data node and sync modules.
${uri}= Set Variable ${ncmpInventoryBasePath}/v1/ch
${headers}= Create Dictionary Content-Type=application/json Authorization=${auth}
${response}= POST On Session CPS_URL ${uri} headers=${headers} data=${jsonDataCreate}
- Should Be Equal As Strings ${response.status_code} 204
+ Should Be Equal As Strings ${response.status_code} 200
Get CM Handle details and confirm it has been registered.
${uri}= Set Variable ${ncmpBasePath}/v1/ch/PNFDemo
@@ -61,7 +61,7 @@ Update data node and sync modules.
${uri}= Set Variable ${ncmpInventoryBasePath}/v1/ch
${headers}= Create Dictionary Content-Type=application/json Authorization=${auth}
${response}= POST On Session CPS_URL ${uri} headers=${headers} data=${jsonDataUpdate}
- Should Be Equal As Strings ${response.status_code} 204
+ Should Be Equal As Strings ${response.status_code} 200
Get CM Handle details and confirm it has been updated.
${uri}= Set Variable ${ncmpBasePath}/v1/ch/PNFDemo
diff --git a/csit/tests/public-properties-query/public-properties-query.robot b/csit/tests/public-properties-query/public-properties-query.robot
new file mode 100644
index 000000000..3a640871b
--- /dev/null
+++ b/csit/tests/public-properties-query/public-properties-query.robot
@@ -0,0 +1,51 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+*** Settings ***
+Documentation Public Properties Query Test
+
+Library Collections
+Library OperatingSystem
+Library RequestsLibrary
+Library BuiltIn
+
+Suite Setup Create Session CPS_URL http://${CPS_CORE_HOST}:${CPS_CORE_PORT}
+
+*** Variables ***
+
+${auth} Basic Y3BzdXNlcjpjcHNyMGNrcyE=
+${ncmpBasePath} /ncmp/v1
+${jsonMatchingQueryParameters} {"publicCmHandleProperties": {"Contact" : "newemailforstore@bookstore.com", "Contact2" : "storeemail2@bookstore.com"}}
+${jsonMissingPropertyQueryParameters} {"publicCmHandleProperties": { "" : "doesnt matter"}}
+
+*** Test Cases ***
+Retrieve CM Handles where query parameters Match
+ ${uri}= Set Variable ${ncmpBasePath}/data/ch/searches
+ ${headers}= Create Dictionary Content-Type=application/json Authorization=${auth}
+ ${response}= POST On Session CPS_URL ${uri} headers=${headers} data=${jsonMatchingQueryParameters}
+ ${responseJson}= Set Variable ${response.json()}
+ Should Be Equal As Strings ${response.status_code} 200
+ Should Contain ${responseJson} PNFDemo
+
+Throw 400 when Structure of Request is Incorrect
+ ${uri}= Set Variable ${ncmpBasePath}/data/ch/searches
+ ${headers}= Create Dictionary Content-Type=application/json Authorization=${auth}
+ ${response}= POST On Session CPS_URL ${uri} headers=${headers} data=${jsonMissingPropertyQueryParameters} expected_status=400
+ Should Be Equal As Strings ${response} <Response [400]>
diff --git a/docs/admin-guide.rst b/docs/admin-guide.rst
index 203151bf8..1bc7f4ff3 100644
--- a/docs/admin-guide.rst
+++ b/docs/admin-guide.rst
@@ -1,6 +1,6 @@
.. This work is licensed under a Creative Commons Attribution 4.0 International License.
.. http://creativecommons.org/licenses/by/4.0
-.. Copyright (C) 2021 Nordix Foundation
+.. Copyright (C) 2021-2022 Nordix Foundation
.. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
.. _adminGuide:
@@ -49,8 +49,6 @@ CPS Log pattern
Change logging level
--------------------
-.. container:: ulist
-
- Curl command 1. Check current log level of "logging.level.org.onap.cps" if it is set to it's default value (INFO)
.. code-block:: java
@@ -193,3 +191,19 @@ Prometheus Metrics can be checked at the following endpoint
.. code::
http://<cps-component-service-name>:8081/manage/prometheus
+
+Naming Validation
+-----------------
+
+As part of the Jakarta 3.1.0 release, CPS has added validation to the names of the following components:
+
+ - Dataspace names
+ - Schema Set names
+ - Anchor names
+ - Cm-Handle identifiers
+
+The following characters along with spaces are no longer valid for naming of these components.
+
+.. code::
+
+ !"#$%&'()*+,./\:;<=>?@[]^`{|}~
diff --git a/docs/api/swagger/cps/openapi.yaml b/docs/api/swagger/cps/openapi.yaml
index 2fc8d7f33..983252f5a 100644
--- a/docs/api/swagger/cps/openapi.yaml
+++ b/docs/api/swagger/cps/openapi.yaml
@@ -15,27 +15,28 @@ info:
x-logo:
url: cps_logo.png
servers:
- - url: /cps/api
+- url: /cps/api
tags:
- - name: cps-admin
- description: cps Admin
- - name: cps-data
- description: cps Data
+- name: cps-admin
+ description: cps Admin
+- name: cps-data
+ description: cps Data
paths:
/v1/dataspaces:
post:
tags:
- - cps-admin
+ - cps-admin
summary: Create a dataspace
description: Create a new dataspace
operationId: createDataspace
parameters:
- - name: dataspace-name
- in: query
- description: dataspace-name
- required: true
- schema:
- type: string
+ - name: dataspace-name
+ in: query
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
responses:
"201":
description: Created
@@ -43,38 +44,130 @@ paths:
text/plain:
schema:
type: string
+ example: my-resource
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "409":
+ description: Conflict
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 409
+ message: Conflicting request
+ details: The request cannot be processed as the resource is in use.
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
+ delete:
+ tags:
+ - cps-admin
+ summary: Delete a dataspace
+ description: Delete a dataspace
+ operationId: deleteDataspace
+ parameters:
+ - name: dataspace-name
+ in: query
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ responses:
+ "204":
+ description: No Content
+ content: {}
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "409":
+ description: Conflict
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 409
+ message: Conflicting request
+ details: The request cannot be processed as the resource is in use.
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
/v1/dataspaces/{dataspace-name}/anchors:
get:
tags:
- - cps-admin
+ - cps-admin
summary: Get anchors
description: "Read all anchors, given a dataspace"
operationId: getAnchors
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
responses:
"200":
description: OK
@@ -90,49 +183,68 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
- "404":
- description: The specified resource was not found
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
post:
tags:
- - cps-admin
+ - cps-admin
summary: Create an anchor
description: Create a new anchor in the given dataspace
operationId: createAnchor
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: schema-set-name
- in: query
- description: schema-set-name
- required: true
- schema:
- type: string
- - name: anchor-name
- in: query
- description: anchor-name
- required: true
- schema:
- type: string
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: schema-set-name
+ in: query
+ description: schema-set-name
+ required: true
+ schema:
+ type: string
+ example: my-schema-set
+ - name: anchor-name
+ in: query
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
responses:
"201":
description: Created
@@ -140,44 +252,79 @@ paths:
text/plain:
schema:
type: string
+ example: my-resource
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "409":
+ description: Conflict
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 409
+ message: Conflicting request
+ details: The request cannot be processed as the resource is in use.
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
/v1/dataspaces/{dataspace-name}/anchors/{anchor-name}:
get:
tags:
- - cps-admin
+ - cps-admin
summary: Get an anchor
description: Read an anchor given an anchor name and a dataspace
operationId: getAnchor
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: anchor-name
- in: path
- description: anchor-name
- required: true
- schema:
- type: string
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: anchor-name
+ in: path
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
responses:
"200":
description: OK
@@ -191,43 +338,61 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
- "404":
- description: The specified resource was not found
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
delete:
tags:
- - cps-admin
+ - cps-admin
summary: Delete an anchor
description: Delete an anchor given an anchor name and a dataspace
operationId: deleteAnchor
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: anchor-name
- in: path
- description: anchor-name
- required: true
- schema:
- type: string
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: anchor-name
+ in: path
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
responses:
"204":
description: No Content
@@ -238,38 +403,62 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
/v1/dataspaces/{dataspace-name}/schema-sets:
post:
tags:
- - cps-admin
+ - cps-admin
summary: Create a schema set
description: Create a new schema set in the given dataspace
operationId: createSchemaSet
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: schema-set-name
- in: query
- description: schema-set-name
- required: true
- schema:
- type: string
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: schema-set-name
+ in: query
+ description: schema-set-name
+ required: true
+ schema:
+ type: string
+ example: my-schema-set
requestBody:
content:
multipart/form-data:
@@ -283,44 +472,79 @@ paths:
text/plain:
schema:
type: string
+ example: my-resource
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "409":
+ description: Conflict
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 409
+ message: Conflicting request
+ details: The request cannot be processed as the resource is in use.
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
/v1/dataspaces/{dataspace-name}/schema-sets/{schema-set-name}:
get:
tags:
- - cps-admin
+ - cps-admin
summary: Get a schema set
description: Read a schema set given a schema set name and a dataspace
operationId: getSchemaSet
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: schema-set-name
- in: path
- description: schema-set-name
- required: true
- schema:
- type: string
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: schema-set-name
+ in: path
+ description: schema-set-name
+ required: true
+ schema:
+ type: string
+ example: my-schema-set
responses:
"200":
description: OK
@@ -334,43 +558,61 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
- "404":
- description: The specified resource was not found
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
delete:
tags:
- - cps-admin
+ - cps-admin
summary: Delete a schema set
description: Delete a schema set given a schema set name and a dataspace
operationId: deleteSchemaSet
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: schema-set-name
- in: path
- description: schema-set-name
- required: true
- schema:
- type: string
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: schema-set-name
+ in: path
+ description: schema-set-name
+ required: true
+ schema:
+ type: string
+ example: my-schema-set
responses:
"204":
description: No Content
@@ -381,59 +623,93 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
"409":
description: Conflict
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 409
+ message: Conflicting request
+ details: The request cannot be processed as the resource is in use.
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
/v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/node:
get:
tags:
- - cps-data
+ - cps-data
summary: Get a node
description: Get a node with an option to retrieve all the children for a given
anchor and dataspace
operationId: getNodeByDataspaceAndAnchor
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: anchor-name
- in: path
- description: anchor-name
- required: true
- schema:
- type: string
- - name: xpath
- in: query
- description: xpath
- required: false
- schema:
- type: string
- default: /
- - name: include-descendants
- in: query
- description: include-descendants
- required: false
- schema:
- type: boolean
- default: false
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: anchor-name
+ in: path
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
+ - name: xpath
+ in: query
+ description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
+ required: false
+ schema:
+ type: string
+ default: /
+ examples:
+ container xpath:
+ value: /shops/bookstore
+ list attributes xpath:
+ value: "/shops/bookstore/categories[@code=1]"
+ - name: include-descendants
+ in: query
+ description: include-descendants
+ required: false
+ schema:
+ type: boolean
+ example: false
+ default: false
responses:
"200":
description: OK
@@ -441,75 +717,100 @@ paths:
application/json:
schema:
type: object
- example:
- child: my_child
- leafList: "leafListElement1, leafListElement2"
- leaf: my_leaf
+ examples:
+ dataSample:
+ $ref: '#/components/examples/dataSample'
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
- "404":
- description: The specified resource was not found
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
x-codegen-request-body-name: xpath
/v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes:
put:
tags:
- - cps-data
+ - cps-data
summary: Replace a node with descendants
description: "Replace a node with descendants for a given dataspace, anchor\
\ and a parent node xpath"
operationId: replaceNode
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: anchor-name
- in: path
- description: anchor-name
- required: true
- schema:
- type: string
- - name: xpath
- in: query
- description: xpath
- required: false
- schema:
- type: string
- default: /
- - name: observed-timestamp
- in: query
- description: observed-timestamp
- required: false
- schema:
- type: string
- example: 2021-03-21T00:10:34.030-0100
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: anchor-name
+ in: path
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
+ - name: xpath
+ in: query
+ description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
+ required: false
+ schema:
+ type: string
+ default: /
+ examples:
+ container xpath:
+ value: /shops/bookstore
+ list attributes xpath:
+ value: "/shops/bookstore/categories[@code=1]"
+ - name: observed-timestamp
+ in: query
+ description: observed-timestamp
+ required: false
+ schema:
+ type: string
+ example: 2021-03-21T00:10:34.030-0100
requestBody:
content:
application/json:
schema:
- type: string
+ type: object
+ examples:
+ dataSample:
+ $ref: '#/components/examples/dataSample'
required: true
responses:
"200":
@@ -518,64 +819,97 @@ paths:
application/json:
schema:
type: object
- example:
- key: value
+ examples:
+ dataSample:
+ value: ""
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
post:
tags:
- - cps-data
+ - cps-data
summary: Create a node
description: Create a node for a given anchor and dataspace
operationId: createNode
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: anchor-name
- in: path
- description: anchor-name
- required: true
- schema:
- type: string
- - name: xpath
- in: query
- description: xpath
- required: false
- schema:
- type: string
- default: /
- - name: observed-timestamp
- in: query
- description: observed-timestamp
- required: false
- schema:
- type: string
- example: 2021-03-21T00:10:34.030-0100
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: anchor-name
+ in: path
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
+ - name: xpath
+ in: query
+ description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
+ required: false
+ schema:
+ type: string
+ default: /
+ examples:
+ container xpath:
+ value: /shops/bookstore
+ list attributes xpath:
+ value: "/shops/bookstore/categories[@code=1]"
+ - name: observed-timestamp
+ in: query
+ description: observed-timestamp
+ required: false
+ schema:
+ type: string
+ example: 2021-03-21T00:10:34.030-0100
requestBody:
content:
application/json:
schema:
- type: string
+ type: object
+ examples:
+ dataSample:
+ $ref: '#/components/examples/dataSample'
required: true
responses:
"201":
@@ -584,63 +918,191 @@ paths:
text/plain:
schema:
type: string
+ example: my-resource
+ "400":
+ description: Bad Request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "409":
+ description: Conflict
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 409
+ message: Conflicting request
+ details: The request cannot be processed as the resource is in use.
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
+ delete:
+ tags:
+ - cps-data
+ summary: Delete a data node
+ description: Delete a datanode for a given dataspace and anchor given a node
+ xpath.
+ operationId: deleteDataNode
+ parameters:
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: anchor-name
+ in: path
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
+ - name: xpath
+ in: query
+ description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
+ required: false
+ schema:
+ type: string
+ default: /
+ examples:
+ container xpath:
+ value: /shops/bookstore
+ list attributes xpath:
+ value: "/shops/bookstore/categories[@code=1]"
+ - name: observed-timestamp
+ in: query
+ description: observed-timestamp
+ required: false
+ schema:
+ type: string
+ example: 2021-03-21T00:10:34.030-0100
+ responses:
+ "204":
+ description: No Content
+ content: {}
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
patch:
tags:
- - cps-data
+ - cps-data
summary: Update node leaves
description: Update a data node leaves for a given dataspace and anchor and
a parent node xpath
operationId: updateNodeLeaves
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: anchor-name
- in: path
- description: anchor-name
- required: true
- schema:
- type: string
- - name: xpath
- in: query
- description: xpath
- required: false
- schema:
- type: string
- default: /
- - name: observed-timestamp
- in: query
- description: observed-timestamp
- required: false
- schema:
- type: string
- example: 2021-03-21T00:10:34.030-0100
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: anchor-name
+ in: path
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
+ - name: xpath
+ in: query
+ description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
+ required: false
+ schema:
+ type: string
+ default: /
+ examples:
+ container xpath:
+ value: /shops/bookstore
+ list attributes xpath:
+ value: "/shops/bookstore/categories[@code=1]"
+ - name: observed-timestamp
+ in: query
+ description: observed-timestamp
+ required: false
+ schema:
+ type: string
+ example: 2021-03-21T00:10:34.030-0100
requestBody:
content:
application/json:
schema:
- type: string
+ type: object
+ examples:
+ dataSample:
+ $ref: '#/components/examples/dataSample'
required: true
responses:
"200":
@@ -649,129 +1111,195 @@ paths:
application/json:
schema:
type: object
- example:
- key: value
+ examples:
+ dataSample:
+ value: ""
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
/v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/list-nodes:
put:
tags:
- - cps-data
- summary: Replace list-node child element(s) under existing parent node
- description: Replace list-node child elements under existing node for a given
- anchor and dataspace
- operationId: replaceListNodeElements
+ - cps-data
+ summary: Replace list content
+ description: "Replace list content under a given parent, anchor and dataspace"
+ operationId: replaceListContent
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: anchor-name
- in: path
- description: anchor-name
- required: true
- schema:
- type: string
- - name: xpath
- in: query
- description: xpath
- required: true
- schema:
- type: string
- - name: observed-timestamp
- in: query
- description: observed-timestamp
- required: false
- schema:
- type: string
- example: 2021-03-21T00:10:34.030-0100
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: anchor-name
+ in: path
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
+ - name: xpath
+ in: query
+ description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
+ required: true
+ schema:
+ type: string
+ examples:
+ container xpath:
+ value: /shops/bookstore
+ list attributes xpath:
+ value: "/shops/bookstore/categories[@code=1]"
+ - name: observed-timestamp
+ in: query
+ description: observed-timestamp
+ required: false
+ schema:
+ type: string
+ example: 2021-03-21T00:10:34.030-0100
requestBody:
content:
application/json:
schema:
- type: string
+ type: object
+ examples:
+ dataSample:
+ $ref: '#/components/examples/dataSample'
required: true
responses:
"200":
- description: Created
+ description: OK
content:
- text/plain:
+ application/json:
schema:
- type: string
+ type: object
+ examples:
+ dataSample:
+ value: ""
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
post:
tags:
- - cps-data
- summary: Add list-node child element(s) under existing parent node
- description: Add list-node child elements to existing node for a given anchor
- and dataspace
- operationId: addListNodeElements
+ - cps-data
+ summary: Add list element(s)
+ description: Add list element(s) to a list for a given anchor and dataspace
+ operationId: addListElements
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: anchor-name
- in: path
- description: anchor-name
- required: true
- schema:
- type: string
- - name: xpath
- in: query
- description: xpath
- required: true
- schema:
- type: string
- - name: observed-timestamp
- in: query
- description: observed-timestamp
- required: false
- schema:
- type: string
- example: 2021-03-21T00:10:34.030-0100
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: anchor-name
+ in: path
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
+ - name: xpath
+ in: query
+ description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
+ required: true
+ schema:
+ type: string
+ examples:
+ container xpath:
+ value: /shops/bookstore
+ list attributes xpath:
+ value: "/shops/bookstore/categories[@code=1]"
+ - name: observed-timestamp
+ in: query
+ description: observed-timestamp
+ required: false
+ schema:
+ type: string
+ example: 2021-03-21T00:10:34.030-0100
requestBody:
content:
application/json:
schema:
- type: string
+ type: object
+ examples:
+ dataSample:
+ $ref: '#/components/examples/dataSample'
required: true
responses:
"201":
@@ -780,57 +1308,86 @@ paths:
text/plain:
schema:
type: string
+ example: my-resource
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
delete:
tags:
- - cps-data
- summary: Delete list-node child element(s) under existing parent node
- description: Delete list-node child elements under existing node for a given
- anchor and dataspace
- operationId: deleteListNodeElements
+ - cps-data
+ summary: Delete one or all list element(s)
+ description: Delete one or all list element(s) for a given anchor and dataspace
+ operationId: deleteListOrListElement
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: anchor-name
- in: path
- description: anchor-name
- required: true
- schema:
- type: string
- - name: xpath
- in: query
- description: xpath
- required: true
- schema:
- type: string
- - name: observed-timestamp
- in: query
- description: observed-timestamp
- required: false
- schema:
- type: string
- example: 2021-03-21T00:10:34.030-0100
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: anchor-name
+ in: path
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
+ - name: xpath
+ in: query
+ description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
+ required: true
+ schema:
+ type: string
+ examples:
+ container xpath:
+ value: /shops/bookstore
+ list attributes xpath:
+ value: "/shops/bookstore/categories[@code=1]"
+ - name: observed-timestamp
+ in: query
+ description: observed-timestamp
+ required: false
+ schema:
+ type: string
+ example: 2021-03-21T00:10:34.030-0100
responses:
"204":
description: No Content
@@ -841,52 +1398,83 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
+ deprecated: true
/v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query:
get:
tags:
- - cps-query
+ - cps-query
summary: Query data nodes
description: Query data nodes for the given dataspace and anchor using CPS path
operationId: getNodesByDataspaceAndAnchorAndCpsPath
parameters:
- - name: dataspace-name
- in: path
- description: dataspace-name
- required: true
- schema:
- type: string
- - name: anchor-name
- in: path
- description: anchor-name
- required: true
- schema:
- type: string
- - name: cps-path
- in: query
- description: cps-path
- required: false
- schema:
- type: string
- default: /
- - name: include-descendants
- in: query
- description: include-descendants
- required: false
- schema:
- type: boolean
- default: false
+ - name: dataspace-name
+ in: path
+ description: dataspace-name
+ required: true
+ schema:
+ type: string
+ example: my-dataspace
+ - name: anchor-name
+ in: path
+ description: anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
+ - name: cps-path
+ in: query
+ description: "For more details on cps path, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
+ required: false
+ schema:
+ type: string
+ default: /
+ examples:
+ container cps path:
+ value: //bookstore
+ list attributes cps path:
+ value: "//categories[@code=1]"
+ - name: include-descendants
+ in: query
+ description: include-descendants
+ required: false
+ schema:
+ type: boolean
+ example: false
+ default: false
responses:
"200":
description: OK
@@ -894,32 +1482,49 @@ paths:
application/json:
schema:
type: object
- example:
- key: value
+ examples:
+ dataSample:
+ $ref: '#/components/examples/dataSample'
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 400
+ message: Bad Request
+ details: The provided request is not valid
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 401
+ message: Unauthorized request
+ details: This request is unauthorized
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
- "404":
- description: The specified resource was not found
+ example:
+ status: 403
+ message: Request Forbidden
+ details: This request is forbidden
+ "500":
+ description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
+ example:
+ status: 500
+ message: Internal Server Error
+ details: Internal Server Error occurred
x-codegen-request-body-name: xpath
components:
schemas:
@@ -929,29 +1534,26 @@ components:
properties:
status:
type: string
- example: "400"
message:
type: string
- example: Dataspace not found
details:
type: string
- example: Dataspace with name D1 does not exist.
AnchorDetails:
title: Anchor details by anchor Name
type: object
properties:
name:
type: string
- example: my_anchor
+ example: my-anchor
dataspaceName:
type: string
- example: my_dataspace
+ example: my-dataspace
schemaSetName:
type: string
- example: my_schema_set
+ example: my-schema-set
MultipartFile:
required:
- - file
+ - file
type: object
properties:
file:
@@ -960,28 +1562,40 @@ components:
format: binary
SchemaSetDetails:
title: Schema set details by dataspace and schemasetName
+ required:
+ - moduleReferences
type: object
properties:
dataspaceName:
type: string
- example: my_dataspace
+ example: my-dataspace
moduleReferences:
type: array
items:
$ref: '#/components/schemas/ModuleReferences'
name:
type: string
- example: my_schema_set
+ example: my-schema-set
ModuleReferences:
title: Module reference object
type: object
properties:
name:
type: string
- example: module_reference_name
+ example: my-module-reference-name
namespace:
type: string
- example: module_reference_namespace
+ example: my-module-reference-namespace
revision:
type: string
- example: module_reference_revision
+ example: my-module-reference-revision
+ examples:
+ dataSample:
+ value:
+ test:bookstore:
+ bookstore-name: Chapters
+ categories:
+ - code: 1
+ name: SciFi
+ - code: 2
+ name: kids
diff --git a/docs/api/swagger/ncmp/openapi-inventory.yaml b/docs/api/swagger/ncmp/openapi-inventory.yaml
index 154a4411d..30896f606 100644
--- a/docs/api/swagger/ncmp/openapi-inventory.yaml
+++ b/docs/api/swagger/ncmp/openapi-inventory.yaml
@@ -86,23 +86,16 @@ components:
$ref: '#/components/schemas/RestInputCmHandle'
updatedCmHandles:
type: array
- example:
- cmHandle: my-cm-handle
- cmHandleProperties:
- add-my-property: add-property
- update-my-property: updated-property
- delete-my-property: ~
- publicCmHandleProperties:
- add-my-property: add-property
- update-my-property: updated-property
- delete-my-property: ~
items:
$ref: '#/components/schemas/RestInputCmHandle'
removedCmHandles:
type: array
+ example:
+ - my-cm-handle1
+ - my-cm-handle2
+ - my-cm-handle3
items:
type: string
- example: "[\"my-cm-handle1\",\"my-cm-handle2\",\"my-cm-handle3\"]"
RestInputCmHandle:
required:
- cmHandle
diff --git a/docs/api/swagger/ncmp/openapi.yaml b/docs/api/swagger/ncmp/openapi.yaml
index b7a65632e..a43190bcc 100644
--- a/docs/api/swagger/ncmp/openapi.yaml
+++ b/docs/api/swagger/ncmp/openapi.yaml
@@ -41,16 +41,6 @@ paths:
sample 3:
value:
resourceIdentifier: "parent=shops,child=bookstore"
- - name: Accept
- in: header
- description: "Accept parameter for response, if accept parameter is null,\
- \ that means client can accept any format."
- required: false
- schema:
- type: string
- enum:
- - application/json
- - application/yang-data+json
- name: options
in: query
description: "options parameter in query, it is mandatory to wrap key(s)=value(s)\
@@ -70,6 +60,17 @@ paths:
sample 3:
value:
options: "(depth=2,fields=book/authors)"
+ - name: topic
+ in: query
+ description: topic parameter in query.
+ required: false
+ allowReserved: true
+ schema:
+ type: string
+ examples:
+ sample 1:
+ value:
+ topic: my-topic-name
responses:
"200":
description: OK
@@ -120,6 +121,18 @@ paths:
status: 500
message: Internal Server Error
details: Internal Server Error occurred
+ "502":
+ description: Bad Gateway
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DmiErrorMessage'
+ example:
+ message: "Bad Gateway Error Message NCMP"
+ dmi-response:
+ http-code: 400
+ body: Bad Request
+
/v1/ch/{cm-handle}/data/ds/ncmp-datastore:passthrough-running:
get:
tags:
@@ -155,16 +168,6 @@ paths:
sample 3:
value:
resourceIdentifier: "parent=shops,child=bookstore"
- - name: Accept
- in: header
- description: "Accept parameter for response, if accept parameter is null,\
- \ that means client can accept any format."
- required: false
- schema:
- type: string
- enum:
- - application/json
- - application/yang-data+json
- name: options
in: query
description: "options parameter in query, it is mandatory to wrap key(s)=value(s)\
@@ -184,6 +187,17 @@ paths:
sample 3:
value:
options: "(depth=2,fields=book/authors)"
+ - name: topic
+ in: query
+ description: topic parameter in query.
+ required: false
+ allowReserved: true
+ schema:
+ type: string
+ examples:
+ sample 1:
+ value:
+ topic: my-topic-name
responses:
"200":
description: OK
@@ -234,6 +248,17 @@ paths:
status: 500
message: Internal Server Error
details: Internal Server Error occurred
+ "502":
+ description: Bad Gateway
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DmiErrorMessage'
+ example:
+ message: "Bad Gateway Error Message NCMP"
+ dmi-response:
+ http-code: 400
+ body: Bad Request
put:
tags:
- network-cm-proxy
@@ -340,6 +365,17 @@ paths:
status: 500
message: Internal Server Error
details: Internal Server Error occurred
+ "502":
+ description: Bad Gateway
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DmiErrorMessage'
+ example:
+ message: "Bad Gateway Error Message NCMP"
+ dmi-response:
+ http-code: 400
+ body: Bad Request
post:
tags:
- network-cm-proxy
@@ -442,6 +478,17 @@ paths:
status: 500
message: Internal Server Error
details: Internal Server Error occurred
+ "502":
+ description: Bad Gateway
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DmiErrorMessage'
+ example:
+ message: "Bad Gateway Error Message NCMP"
+ dmi-response:
+ http-code: 400
+ body: Bad Request
delete:
tags:
- network-cm-proxy
@@ -539,6 +586,17 @@ paths:
status: 500
message: Internal Server Error
details: Internal Server Error occurred
+ "502":
+ description: Bad Gateway
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DmiErrorMessage'
+ example:
+ message: "Bad Gateway Error Message NCMP"
+ dmi-response:
+ http-code: 400
+ body: Bad Request
patch:
tags:
- network-cm-proxy
@@ -639,6 +697,17 @@ paths:
status: 500
message: Internal Server Error
details: Internal Server Error occurred
+ "502":
+ description: Bad Gateway
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DmiErrorMessage'
+ example:
+ message: "Bad Gateway Error Message NCMP"
+ dmi-response:
+ http-code: 400
+ body: Bad Request
/v1/ch/{cm-handle}/modules:
get:
tags:
@@ -664,7 +733,7 @@ paths:
schema:
type: array
items:
- $ref: '#/components/schemas/ModuleReference'
+ $ref: '#/components/schemas/RestModuleReference'
"400":
description: Bad Request
content:
@@ -851,7 +920,24 @@ components:
type: string
details:
type: string
- ModuleReference:
+ # DMI Server Exception Schema
+ DmiErrorMessage:
+ title: DMI Error Message
+ type: object
+ properties:
+ message:
+ type: string
+ example: "Bad Gateway Error Message NCMP"
+ dmi-response:
+ type: object
+ properties:
+ http-code:
+ type: integer
+ example: 400
+ body:
+ type: string
+ example: Bad Request
+ RestModuleReference:
title: Module reference details
type: object
properties:
diff --git a/docs/cps-path.rst b/docs/cps-path.rst
index bc46681d1..e8a75d9cf 100644
--- a/docs/cps-path.rst
+++ b/docs/cps-path.rst
@@ -1,6 +1,6 @@
.. This work is licensed under a Creative Commons Attribution 4.0 International License.
.. http://creativecommons.org/licenses/by/4.0
-.. Copyright (C) 2021 Nordix Foundation
+.. Copyright (C) 2021-2022 Nordix Foundation
.. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
.. _design:
@@ -20,17 +20,137 @@ The CPS path parameter is used for querying xpaths. CPS path is inspired by the
This section describes the functionality currently supported by CPS Path.
-Sample Data
-===========
+Sample Yang Model
+=================
-The xml below describes some basic data to be used to illustrate the CPS Path functionality.
+.. code-block::
+
+ module stores {
+ yang-version 1.1;
+ namespace "org:onap:ccsdk:sample";
+
+ prefix book-store;
+
+ revision "2020-09-15" {
+ description
+ "Sample Model";
+ }
+ container shops {
+
+ container bookstore {
+
+ leaf bookstore-name {
+ type string;
+ }
+
+ leaf name {
+ type string;
+ }
+
+ list categories {
+
+ key "code";
+
+ leaf code {
+ type uint16;
+ }
+
+ leaf name {
+ type string;
+ }
+
+ leaf numberOfBooks {
+ type uint16;
+ }
+
+ container books {
+
+ list book {
+ key title;
+
+ leaf title {
+ type string;
+ }
+ leaf price {
+ type uint16;
+ }
+ leaf-list label {
+ type string;
+ }
+ leaf-list edition {
+ type string;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+**Note.** 'categories' is a Yang List and 'code' is its key leaf. All other data nodes are Yang Containers. 'label' and 'edition' are both leaf-lists.
+
+**Note.** CPS accepts only json data. The xml data presented here is for illustration purposes only.
+
+The json and xml below describes some basic data to be used to illustrate the CPS Path functionality.
+
+Sample Data in Json
+===================
+
+.. code-block:: json
+
+ {
+ "shops": {
+ "bookstore": {
+ "bookstore-name": "Chapters",
+ "name": "Chapters",
+ "categories": [
+ {
+ "code": 1,
+ "name": "SciFi",
+ "numberOfBooks": 2,
+ "books": {
+ "book": [
+ {
+ "title": "2001: A Space Odyssey",
+ "price": 5,
+ "label": ["sale", "classic"],
+ "edition": ["1968", "2018"]
+ },
+ {
+ "title": "Dune",
+ "price": 5,
+ "label": ["classic"],
+ "edition": ["1965"]
+ }
+ ]
+ }
+ },
+ {
+ "code": 2,
+ "name": "Kids",
+ "numberOfBooks": 1,
+ "books": {
+ "book": [
+ {
+ "title": "Matilda"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ }
+
+Sample Data in XML
+==================
.. code-block:: xml
<shops>
<bookstore name="Chapters">
<bookstore-name>Chapters</bookstore-name>
- <categories code="1" name="SciFi" numberOfBooks="2">
+ <categories code=1 name="SciFi" numberOfBooks="2">
<books>
<book title="2001: A Space Odyssey" price="5">
<label>sale</label>
@@ -44,7 +164,7 @@ The xml below describes some basic data to be used to illustrate the CPS Path fu
</book>
</books>
</categories>
- <categories code="2" name="Kids" numberOfBooks="1">
+ <categories code=2 name="Kids" numberOfBooks="1">
<books>
<book title="Matilda" />
</books>
@@ -52,8 +172,6 @@ The xml below describes some basic data to be used to illustrate the CPS Path fu
</bookstore>
</shops>
-**Note.** 'categories' is a Yang List and 'code' is its key leaf. All other data nodes are Yang Containers. 'label' and 'edition' are both leaf-lists.
-
General Notes
=============
@@ -79,12 +197,14 @@ absolute-path
**Examples**
- ``/shops/bookstore``
- - ``/shops/bookstore/categories[@code=1]``
- - ``/shops/bookstore/categories[@code=1]/book``
+ - ``/shops/bookstore/categories[@code='1']/books``
+ - ``/shops/bookstore/categories[@code='1']/books/book[@title='2001: A Space Odyssey']``
**Limitations**
- Absolute paths must start with the top element (data node) as per the model tree.
- Each list reference must include a valid instance reference to the key for that list. Except when it is the last element.
+ - The Absolute path to list with integer key will not work. It needs to be surrounded with a single quote ([@code='1'])
+ as if it is a string. This will be fixed in `CPS-961 <https://jira.onap.org/browse/CPS-961>`_
descendant-path
---------------
@@ -95,7 +215,7 @@ descendant-path
**Examples**
- ``//bookstore``
- - ``//categories[@code=1]/book``
+ - ``//categories[@code='1']/books``
- ``//bookstore/categories``
**Limitations**
@@ -113,7 +233,7 @@ leaf-conditions
- ``/shops/bookstore/categories[@numberOfBooks=1]``
- ``//categories[@name="Kids"]``
- ``//categories[@name='Kids']``
- - ``//categories[@code=1]/books/book[@title='Dune' and @price=5]``
+ - ``//categories[@code='1']/books/book[@title='Dune' and @price=5]``
**Limitations**
- Only the last list or container can be queried leaf values. Any ancestor list will have to be referenced by its key name-value pair(s).
@@ -156,9 +276,9 @@ The ancestor axis can be added to any CPS path query but has to be the last part
**Examples**
- ``//book/ancestor::categories``
- - ``//categories[@genre="SciFi"]/book/ancestor::bookstore``
- - ``book/ancestor::categories[@code=1]/books``
- - ``//book/label[text()="classic"]/ancestor::shop``
+ - ``//categories[@code='2']/books/ancestor::bookstore``
+ - ``//book/ancestor::categories[@code='1']/books``
+ - ``//book/label[text()="classic"]/ancestor::shops``
**Limitations**
- Ancestor list elements can only be addressed using the list key leaf.
diff --git a/docs/deployment.rst b/docs/deployment.rst
index 6f450c123..06e1ddcc0 100644
--- a/docs/deployment.rst
+++ b/docs/deployment.rst
@@ -7,13 +7,13 @@
.. _deployment:
CPS Deployment
-==============
+##############
.. contents::
:depth: 2
CPS OOM Charts
---------------
+==============
The CPS kubernetes chart is located in the `OOM repository <https://github.com/onap/oom/tree/master/kubernetes/cps>`_.
This chart includes different cps components referred as <cps-component-name> further in the document are listed below:
@@ -26,7 +26,8 @@ This chart includes different cps components referred as <cps-component-name> fu
Please refer to the `OOM documentation <https://docs.onap.org/projects/onap-oom/en/latest/oom_user_guide.html>`_ on how to install and deploy ONAP.
Installing or Upgrading CPS Components
---------------------------------------
+======================================
+
The assumption is you have cloned the charts from the OOM repository into a local directory.
**Step 1** Go to the cps charts and edit properties in values.yaml files to make any changes to particular cps component if required.
@@ -91,7 +92,7 @@ After deploying cps, keep monitoring the cps pods until they come up.
kubectl get pods -n <namespace> | grep <cps-component-name>
Restarting a faulty component
------------------------------
+=============================
Each cps component can be restarted independently by issuing the following command:
.. code-block:: bash
@@ -102,7 +103,7 @@ Each cps component can be restarted independently by issuing the following comma
.. _cps_common_credentials_retrieval:
Credentials Retrieval
----------------------
+=====================
Application and database credentials are kept in Kubernetes secrets. They are defined as external secrets in the
values.yaml file to be used across different components as :
@@ -161,8 +162,9 @@ Additional Cps-Core Customizations
==================================
The following table lists some properties that can be specified as Helm chart
-values to configure the application to be deployed. This list is not
-exhaustive.
+values to configure the application to be deployed. This list is not exhaustive.
+
+Any spring supported property can be configured by providing in ``config.additional.<spring-supported-property-name>: value`` Example: config.additional.spring.datasource.hikari.maximumPoolSize: 30
+---------------------------------------+---------------------------------------------------------------------------------------------------------+-------------------------------+
| Property | Description | Default Value |
@@ -280,6 +282,10 @@ exhaustive.
| notification.async.executor. | | |
| thread-name-prefix | | |
+---------------------------------------+---------------------------------------------------------------------------------------------------------+-------------------------------+
+| config.additional. | Specifies number of database connections between database and application. | ``10`` |
+| spring.datasource.hikari. | This property controls the maximum size that the pool is allowed to reach, | |
+| maximumPoolSize | including both idle and in-use connections. | |
++---------------------------------------+---------------------------------------------------------------------------------------------------------+-------------------------------+
CPS-Core Docker Installation
============================
diff --git a/docs/index.rst b/docs/index.rst
index 62ba5e8b1..eaf36466f 100755
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -9,7 +9,10 @@
.. _cps-framework-doc:
CPS Documentation
------------------
+#################
+
+CPS Core
+========
.. toctree::
:maxdepth: 1
@@ -22,12 +25,12 @@ CPS Documentation
deployment.rst
release-notes.rst
-DMI-Plugin Documentation
-------------------------
+DMI-Plugin
+==========
* :ref:`DMI-Plugin<onap-cps-ncmp-dmi-plugin:master_index>`
-CPS-Temporal Documentation
---------------------------
+CPS Temporal
+============
* :ref:`CPS-Temporal<onap-cps-cps-temporal:master_index>`
diff --git a/docs/overview.rst b/docs/overview.rst
index 4b69dd810..cde6f6db6 100644
--- a/docs/overview.rst
+++ b/docs/overview.rst
@@ -4,7 +4,7 @@
.. _overview:
CPS Overview
-============
+############
The Configuration Persistence Service (CPS) is a platform component that is designed to serve as a
data repository for runtime data that needs persistence.
@@ -28,10 +28,10 @@ Types of data that is stored:
configuration and operational parameters depending on how they are used.
CPS Components
---------------
+==============
CPS-Core
-########
+--------
This is the component of CPS which encompasses the generic storage of Yang module data.
**NCMP**
@@ -43,13 +43,13 @@ NCMP accesses all network Data-Model-Inventory (DMI) information via NCMP-DMI-Pl
even though CPS-Core could be deployed without the NCMP extension.
NCMP-DMI-Plugin
-####################
+---------------
The Data-Model-Inventory (DMI) Plugin is a rest interface used to synchronize CM-Handles data between CPS and DMI through the DMI-Plugin.
This is built previously from the CPS-NF-Proxy component.
CPS-Temporal
-############
+------------
This service is responsible to provide a time oriented perspective for
operational network data. It provides features to store and retrieve sequences
@@ -57,14 +57,8 @@ of configurations or states along with the associated times when they occurred
or have been observed.
CPS Project
------------
-
-Wiki: `Configuration Persistence Service Project <https://wiki.onap.org/display/DW/Configuration+Persistence+Service+Project>`_
-
-Contact Information
--------------------
-
-onap-discuss@lists.onap.org
+===========
-Meeting details `Join <https://zoom.us/j/836561560?pwd=TTZNcFhXTWYxMmZ4SlgzcVZZQXluUT09>`_
-`Agenda <https://wiki.onap.org/pages/viewpage.action?pageId=111117075>`_
+* Wiki: `Configuration Persistence Service Project <https://wiki.onap.org/display/DW/Configuration+Persistence+Service+Project>`_
+* Contact Information: onap-discuss@lists.onap.org
+* Meeting details: `Join <https://zoom.us/j/836561560?pwd=TTZNcFhXTWYxMmZ4SlgzcVZZQXluUT09>`_ & `Agenda <https://wiki.onap.org/pages/viewpage.action?pageId=111117075>`_
diff --git a/docs/release-notes.rst b/docs/release-notes.rst
index 0ca0547fa..a584b580a 100755
--- a/docs/release-notes.rst
+++ b/docs/release-notes.rst
@@ -5,11 +5,8 @@
.. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
.. _release_notes:
-
-
-=================
CPS Release Notes
-=================
+#################
.. contents::
:depth: 2
@@ -19,10 +16,45 @@ CPS Release Notes
.. * * * JAKARTA * * *
.. ========================
-Version: 3.0.0-SNAPSHOT
-=======================
+Version: 3.1.0
+==============
++--------------------------------------+--------------------------------------------------------+
+| **CPS Project** | |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Docker images** | onap/cps-and-ncmp:3.1.0 |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Release designation** | 3.1.0 Jakarta |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Release date** | |
+| | |
++--------------------------------------+--------------------------------------------------------+
+
+Features
+--------
+ - `CPS-322 <https://jira.onap.org/browse/CPS-322>`_ Implement additional validation for names and identifiers
+
+Version: 3.0.0
+==============
+
+Release Data
+------------
-This section lists the main changes & fixes merged into master (snapshot) version of CPS-NCMP. This information is here to assist developers that want experiment/test using our latest code bases directly. Stability of this is not guaranteed.
++--------------------------------------+--------------------------------------------------------+
+| **CPS Project** | |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Docker images** | onap/cps-and-ncmp:3.0.0 |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Release designation** | 3.0.0 Jakarta |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Release date** | 2022 March 15 |
+| | |
++--------------------------------------+--------------------------------------------------------+
Features
--------
@@ -33,6 +65,8 @@ Features
- `CPS-741 <https://jira.onap.org/browse/CPS-741>`_ Re sync after removing cm handles
- `CPS-777 <https://jira.onap.org/browse/CPS-777>`_ Ensure all DMI operations use POST method
- `CPS-780 <https://jira.onap.org/browse/CPS-780>`_ Add examples for parameters, request and response in openapi yaml for cps-core
+ - `CPS-789 <https://jira.onap.org/browse/CPS-789>`_ CPS Data Updated Event Schema V2 to support delete operation
+ - `CPS-791 <https://jira.onap.org/browse/CPS-791>`_ CPS-Core sends delete notification event
- `CPS-817 <https://jira.onap.org/browse/CPS-817>`_ Create Endpoint For Get Cm Handles (incl. public properties) By Name
- `CPS-837 <https://jira.onap.org/browse/CPS-837>`_ Add Remove and Update properties (DMI and Public) as part of CM Handle Registration update
@@ -59,6 +93,9 @@ Null can no longer be passed within the dmi plugin service names when registerin
`CPS-837 <https://jira.onap.org/browse/CPS-837>`_ null is now used to indicate if a property should be removed as part
of cm handle registration.
+The Absolute path to list with integer key will not work. Please refer `CPS-961 <https://jira.onap.org/browse/CPS-961>`_
+for more information.
+
*Known Vulnerabilities*
None
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
index 5a3d2f17f..3b3441a80 100644
--- a/docs/requirements-docs.txt
+++ b/docs/requirements-docs.txt
@@ -1 +1,3 @@
-lfdocs-conf \ No newline at end of file
+lfdocs-conf
+sphinx>=4.2.0 # BSD
+sphinx-rtd-theme>=1.0.0 # MIT
diff --git a/jacoco-report/pom.xml b/jacoco-report/pom.xml
index d42d89ab0..d1181d367 100644
--- a/jacoco-report/pom.xml
+++ b/jacoco-report/pom.xml
@@ -5,7 +5,7 @@
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/pom.xml b/pom.xml
index 87398bd24..23ef44bb5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@
<groupId>org.onap.cps</groupId>
<artifactId>cps-aggregator</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>cps</name>
diff --git a/releases/3.0.0-container.yaml b/releases/3.0.0-container.yaml
new file mode 100644
index 000000000..f227bdbf5
--- /dev/null
+++ b/releases/3.0.0-container.yaml
@@ -0,0 +1,8 @@
+distribution_type: container
+container_release_tag: 3.0.0
+project: cps
+log_dir: cps-maven-docker-stage-master/504/
+ref: a1129b696f3197fc7d8a3b63bcd84b5b2dd8874e
+containers:
+ - name: 'cps-and-ncmp'
+ version: '3.0.0-20220315T180237Z'
diff --git a/releases/3.0.0.yaml b/releases/3.0.0.yaml
new file mode 100644
index 000000000..60dd8116d
--- /dev/null
+++ b/releases/3.0.0.yaml
@@ -0,0 +1,4 @@
+distribution_type: maven
+log_dir: cps-maven-stage-master/504/
+project: cps
+version: 3.0.0
diff --git a/spotbugs/pom.xml b/spotbugs/pom.xml
index 50cef487e..df033a3f4 100644
--- a/spotbugs/pom.xml
+++ b/spotbugs/pom.xml
@@ -25,7 +25,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>spotbugs</artifactId>
- <version>3.0.0-SNAPSHOT</version>
+ <version>3.1.0-SNAPSHOT</version>
<properties>
<nexusproxy>https://nexus.onap.org</nexusproxy>
@@ -33,6 +33,18 @@
<snapshotNexusPath>/content/repositories/snapshots/</snapshotNexusPath>
</properties>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <version>2.8.2</version>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+
<distributionManagement>
<repository>
<id>ecomp-releases</id>
diff --git a/version.properties b/version.properties
index 17f2daa6c..870b994b7 100755
--- a/version.properties
+++ b/version.properties
@@ -1,5 +1,5 @@
# ============LICENSE_START=======================================================
-# Copyright (C) 2021 Nordix Foundation
+# Copyright (C) 2021-2022 Nordix Foundation
# Modifications Copyright (C) 2022 Bell Canada.
# ================================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,7 +21,7 @@
# because they are used in Jenkins, whose plug-in doesn't support this
major=3
-minor=0
+minor=1
patch=0
base_version=${major}.${minor}.${patch}