Merge "[STRIMZI] Add strimzi kafka notes"
authorToine Siebelink <toine.siebelink@est.tech>
Fri, 29 Apr 2022 15:33:04 +0000 (15:33 +0000)
committerGerrit Code Review <gerrit@onap.org>
Fri, 29 Apr 2022 15:33:04 +0000 (15:33 +0000)
132 files changed:
INFO.yaml
checkstyle/pom.xml
checkstyle/src/main/CopyrightCheck.py [new file with mode: 0644]
checkstyle/src/main/resources/copyright-template.txt [new file with mode: 0644]
checkstyle/src/main/resources/ignore-files-config.csv [new file with mode: 0644]
checkstyle/src/main/resources/project-committers-config.csv [new file with mode: 0644]
checkstyle/src/main/test_CopyrightCheck.py [new file with mode: 0644]
cps-application/pom.xml
cps-bom/pom.xml
cps-dependencies/pom.xml
cps-events/pom.xml
cps-ncmp-rest/docs/openapi/components.yaml
cps-ncmp-rest/docs/openapi/ncmp-inventory.yml
cps-ncmp-rest/docs/openapi/ncmp.yml
cps-ncmp-rest/docs/openapi/openapi.yml
cps-ncmp-rest/pom.xml
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapper.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NcmpRestInputMapperSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy
cps-ncmp-rest/src/test/resources/dmi_registration_all_singing_and_dancing.json
cps-ncmp-rest/src/test/resources/dmi_registration_updates_only.json
cps-ncmp-rest/src/test/resources/dmi_registration_without_properties.json
cps-ncmp-service/pom.xml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetriever.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandle.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelCmHandlesList.java [deleted file]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncService.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/NcmpServiceCmHandle.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/YangModelCmHandleRetrieverSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncServiceSpec.groovy [moved from cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplModelSyncSpec.groovy with 65% similarity]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/YangModelCmHandleSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy
cps-parent/pom.xml
cps-path-parser/pom.xml
cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java [new file with mode: 0644]
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java [new file with mode: 0755]
cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy
cps-rest/docs/openapi/cpsAdmin.yml
cps-rest/pom.xml
cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
cps-ri/pom.xml
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java
cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java
cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java
cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java [new file with mode: 0644]
cps-ri/src/main/java/org/onap/cps/spi/utils/TimeLimiterProvider.java [new file with mode: 0644]
cps-ri/src/main/resources/hibernate.cfg.xml [new file with mode: 0644]
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy [new file with mode: 0644]
cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy [new file with mode: 0644]
cps-ri/src/test/resources/data/cps-path-query.sql
cps-ri/src/test/resources/data/fragment.sql
cps-ri/src/test/resources/hibernate.cfg.xml [new file with mode: 0644]
cps-service/pom.xml
cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java
cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java
cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java
cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java
cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java
cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionManagerException.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/spi/exceptions/SessionTimeoutException.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java
cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/YangTextSchemaSourceSetCacheSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy [moved from cps-service/src/test/groovy/org/onap/cps/utils/YangTextSchemaSourceSetSpec.groovy with 88% similarity]
csit/data/cmHandleRegistration.json [deleted file]
csit/plans/cps/test.properties
csit/plans/cps/testplan.txt
csit/tests/cps-model-sync/cps-model-sync.robot
csit/tests/public-properties-query/public-properties-query.robot [new file with mode: 0644]
docs/admin-guide.rst
docs/api/swagger/cps/openapi.yaml
docs/api/swagger/ncmp/openapi-inventory.yaml
docs/api/swagger/ncmp/openapi.yaml
docs/cps-path.rst
docs/deployment.rst
docs/index.rst
docs/overview.rst
docs/release-notes.rst
docs/requirements-docs.txt
jacoco-report/pom.xml
pom.xml
releases/3.0.0-container.yaml [new file with mode: 0644]
releases/3.0.0.yaml [new file with mode: 0644]
spotbugs/pom.xml
version.properties

index 279ec85..610fd0e 100755 (executable)
--- 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:
index 07e6cf9..8d11742 100644 (file)
@@ -2,6 +2,7 @@
 <!--
   ============LICENSE_START=======================================================
   Copyright (C) 2020 Pantheon.tech
+  Modifications Copyright (C) 2022 Nordix Foundation
   ================================================================================
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.onap.cps</groupId>
     <artifactId>checkstyle</artifactId>
-    <version>3.0.0-SNAPSHOT</version>
+    <version>3.1.0-SNAPSHOT</version>
+
+    <profiles>
+        <profile>
+            <id>Windows</id>
+            <activation>
+                <os>
+                    <family>Windows</family>
+                </os>
+            </activation>
+            <properties>
+                <script.executor>python</script.executor>
+            </properties>
+        </profile>
+        <profile>
+            <id>unix</id>
+            <activation>
+                <os>
+                    <family>unix</family>
+                </os>
+            </activation>
+            <properties>
+                <script.executor>python3</script.executor>
+            </properties>
+        </profile>
+    </profiles>
 
     <properties>
         <nexusproxy>https://nexus.onap.org</nexusproxy>
         <snapshotNexusPath>/content/repositories/snapshots/</snapshotNexusPath>
     </properties>
 
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-deploy-plugin</artifactId>
+                    <version>2.8.2</version>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>exec-maven-plugin</artifactId>
+                <version>1.6.0</version>
+                <executions>
+                    <execution>
+                        <id>copyright-check</id>
+                        <phase>verify</phase>
+                        <goals>
+                            <goal>exec</goal>
+                        </goals>
+                        <configuration>
+                            <executable>${script.executor}</executable>
+                            <workingDirectory>../checkstyle/src/main/</workingDirectory>
+                            <arguments>
+                                <argument>CopyrightCheck.py</argument>
+                                <argument>resources/project-committers-config.csv</argument>
+                                <argument>resources/copyright-template.txt</argument>
+                                <argument>resources/ignore-files-config.csv</argument>
+                            </arguments>
+                            <successCodes>
+                                <successCode>0</successCode>
+                                <successCode>1</successCode>
+                            </successCodes>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
     <distributionManagement>
         <repository>
             <id>ecomp-releases</id>
diff --git a/checkstyle/src/main/CopyrightCheck.py b/checkstyle/src/main/CopyrightCheck.py
new file mode 100644 (file)
index 0000000..8f1dbff
--- /dev/null
@@ -0,0 +1,262 @@
+#  ============LICENSE_START=======================================================
+#  Copyright (C) 2022 Nordix Foundation
+#  ================================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+#  SPDX-License-Identifier: Apache-2.0
+#  ============LICENSE_END=========================================================
+
+import subprocess
+import csv
+import re
+import datetime
+
+#constants
+import sys
+
+COMMITTERS_CONFIG_FILE = ''
+TEMPLATE_COPYRIGHT_FILE = ''
+IGNORE_FILE = ''
+if len(sys.argv) == 4:
+    COMMITTERS_CONFIG_FILE = sys.argv[1]
+    TEMPLATE_COPYRIGHT_FILE = sys.argv[2]
+    IGNORE_FILE = sys.argv[3]
+
+BANNER = '=' * 120
+
+def main():
+    print(BANNER + '\nCopyright Check Python Script:')
+    PermissionsCheck()
+
+    committerEmailExtension = GetCommitterEmailExtension()
+    projectCommitters = ReadProjectCommittersConfigFile()
+
+    CheckCommitterInConfigFile(committerEmailExtension, projectCommitters)
+
+    alteredFiles = FindAlteredFiles()
+
+    if alteredFiles:
+        issueCounter = CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension)
+    else:
+        issueCounter = 0
+
+    print(str(issueCounter) + ' issue(s) found after '+ str(len(alteredFiles)) + ' altered file(s) checked')
+    print(BANNER)
+
+
+# Check that Script has access to command line functions to use git
+def PermissionsCheck():
+   if 'permission denied' in subprocess.run('git', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').lower():
+       print('Error, I may not have the necessary permissions. Exiting...')
+       print(BANNER)
+       sys.exit()
+   else:
+       return
+
+# Returns List of Strings of file tracked by git which have been changed/added
+def FindAlteredFiles():
+    ignoreFilePaths = GetIgnoredFiles()
+
+    #Before Stage lower case d removes deleted files
+    stream = subprocess.run('git diff --name-only --diff-filter=d', shell=True, stdout=subprocess.PIPE)
+    fileNames = stream.stdout.decode('utf-8')
+    #Staged
+    stream = subprocess.run('git diff --name-only --cached --diff-filter=d', shell=True, stdout=subprocess.PIPE)
+    fileNames += '\n' + stream.stdout.decode('utf-8')
+    #New committed
+    stream = subprocess.run('git diff --name-only HEAD^ HEAD --diff-filter=d', shell=True, stdout=subprocess.PIPE)
+    fileNames += '\n' + stream.stdout.decode('utf-8')
+
+    #String to list of strings
+    alteredFiles = fileNames.split("\n")
+
+    #Remove duplicates
+    alteredFiles = list(dict.fromkeys(alteredFiles))
+
+    #Remove blank string(s)
+    alteredFiles = list(filter(None, alteredFiles))
+
+    #Remove ignored-extensions
+    alteredFiles = list(filter(lambda fileName: not re.match("|".join(ignoreFilePaths), fileName), alteredFiles))
+
+    return alteredFiles
+
+# Get the email of the most recent committer
+def GetCommitterEmailExtension():
+    email = subprocess.run('git show -s --format=\'%ce\'', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip('\n')
+    return email[email.index('@'):]
+
+# Read the config file with names of companies and respective email extensions
+def ReadProjectCommittersConfigFile():
+    try:
+        with open(COMMITTERS_CONFIG_FILE, 'r') as file:
+            reader = csv.reader(file, delimiter=',')
+            projectCommitters = {row[0]:row[1] for row in reader}
+        projectCommitters.pop('email')  #Remove csv header
+    except FileNotFoundError:
+        print('Unable to open Project Committers Config File, have the command line arguments been set?')
+        print(BANNER)
+        sys.exit()
+    return projectCommitters
+
+def CheckCommitterInConfigFile(committerEmailExtension, projectCommitters):
+    if not committerEmailExtension in projectCommitters:
+        print('Error, Committer email is not included in config file.')
+        print('If your company is new to the project please make appropriate changes to project-committers-config.csv')
+        print('for Copyright Check to work.')
+        print('Exiting...')
+        print(BANNER)
+        sys.exit()
+    else:
+        return True
+
+# Read config file with list of files to ignore
+def GetIgnoredFiles():
+    try:
+        with open(IGNORE_FILE, 'r') as file:
+            reader = csv.reader(file)
+            ignoreFilePaths = [row[0] for row in reader]
+        ignoreFilePaths.pop(0)  #Remove csv header
+        ignoreFilePaths = [filePath.replace('*', '.*') for filePath in ignoreFilePaths]
+    except FileNotFoundError:
+        print('Unable to open File Ignore Config File, have the command line arguments been set?')
+        print(BANNER)
+        sys.exit()
+    return ignoreFilePaths
+
+# Read the template copyright file
+def GetCopyrightTemplate():
+    try:
+        with open(TEMPLATE_COPYRIGHT_FILE, 'r') as file:
+            copyrightTemplate = file.readlines()
+    except FileNotFoundError:
+        print('Unable to open Template Copyright File, have the command line arguments been set?')
+        print(BANNER)
+        sys.exit()
+    return copyrightTemplate
+
+def GetProjectRootDir():
+    return subprocess.run('git rev-parse --show-toplevel', shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip('\n') + '/'
+
+# Get the Copyright from the altered file
+def ParseFileCopyright(fileObject):
+    global issueCounter
+    copyrightFlag = False
+    copyrightInFile = {}
+    lineNumber = 1
+    for line in fileObject:
+        if 'LICENSE_START' in line:
+            copyrightFlag = True
+        if copyrightFlag:
+            copyrightInFile[lineNumber] = line
+        if 'LICENSE_END' in line:
+            break
+        lineNumber += 1
+
+    if not copyrightFlag:
+        print(fileObject.name + ' | no copyright found')
+        return {}, {}
+
+    copyrightSignatures = {}
+    copyrightLineNumbers = list(copyrightInFile.keys())
+    #Capture signature lines after LICENSE_START line
+    for lineNumber in copyrightLineNumbers:
+        if '=' not in copyrightInFile[lineNumber]:
+            copyrightSignatures[lineNumber] = copyrightInFile[lineNumber]
+            copyrightInFile.pop(lineNumber)
+        elif 'LICENSE_START' not in copyrightInFile[lineNumber]:
+            break
+
+    return (copyrightInFile, copyrightSignatures)
+
+# Remove the Block comment syntax
+def RemoveCommentBlock(fileCopyright):
+    # Comment Characters can very depending on file # *..
+    endOfCommentsIndex = list(fileCopyright.values())[0].index('=')
+    for key in fileCopyright:
+        fileCopyright[key] = fileCopyright[key][endOfCommentsIndex:]
+        if fileCopyright[key] == '':
+            fileCopyright[key] = '\n'
+
+    return fileCopyright
+
+def CheckCopyrightForFiles(alteredFiles, projectCommitters, committerEmailExtension):
+    issueCounter = 0
+    templateCopyright = GetCopyrightTemplate() #Get Copyright Template
+    projectRootDir = GetProjectRootDir()
+
+    for fileName in alteredFiles: # Not removed files
+        try:
+            with open(projectRootDir + fileName, 'r') as fileObject:
+                (fileCopyright, fileSignatures) = ParseFileCopyright(fileObject)
+
+            #Empty dict evaluates to false
+            if fileCopyright and fileSignatures:
+                fileCopyright = RemoveCommentBlock(fileCopyright)
+                issueCounter += CheckCopyrightFormat(fileCopyright, templateCopyright, projectRootDir + fileName)
+                committerCompany = projectCommitters[committerEmailExtension]
+                issueCounter += CheckCopyrightSignature(fileSignatures, committerCompany, projectRootDir + fileName)
+            else:
+                issueCounter += 1
+
+        except FileNotFoundError:
+            issueCounter += 1
+            print('Unable to find file ' + projectRootDir + fileName)
+    return issueCounter
+
+# Check that the filecopyright matches the template copyright and print comparison
+def CheckCopyrightFormat(copyrightInFile, templateCopyright, filePath):
+    issueCounter = 0
+    errorWithComparison = ''
+    for copyrightInFileKey, templateLine in zip(copyrightInFile, templateCopyright):
+        if copyrightInFile[copyrightInFileKey] != templateLine:
+            issueCounter += 1
+            errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' read \t  ' + repr(copyrightInFile[copyrightInFileKey]) + '\n'
+            errorWithComparison += filePath + ' | line ' + '{:2}'.format(copyrightInFileKey) + ' expected ' + repr(templateLine) + '\n'
+    if errorWithComparison != '':
+        print(errorWithComparison.rstrip('\n'))
+    return issueCounter
+
+# Check the signatures and compare with committer signature and current year
+def CheckCopyrightSignature(copyrightSignatures, committerCompany, filePath):
+    issueCounter = 0
+    errorWithSignature = ''
+    signatureExists = False #signatureExistsForCommitter
+    afterFirstLine = False #afterFirstCopyright
+    for key in copyrightSignatures:
+        if afterFirstLine and 'Modifications Copyright' not in copyrightSignatures[key]:
+            issueCounter += 1
+            errorWithSignature += filePath + ' | line ' + str(key) + ' expected Modifications Copyright\n'
+        elif not afterFirstLine and 'Copyright' not in copyrightSignatures[key]:
+            issueCounter += 1
+            errorWithSignature += filePath + ' | line ' + str(key) + ' expected Copyright\n'
+        if committerCompany in copyrightSignatures[key]:
+            signatureExists = True
+            signatureYear = int(re.findall(r'\d+', copyrightSignatures[key])[-1])
+            currentYear = datetime.date.today().year
+            if signatureYear != currentYear:
+                issueCounter += 1
+                errorWithSignature += filePath + ' | line ' + str(key) + ' update year to include ' + str(currentYear) + '\n'
+        afterFirstLine = True
+
+    if not signatureExists:
+        issueCounter += 1
+        errorWithSignature += filePath + ' | missing company name and year for ' + committerCompany
+
+    if errorWithSignature != '':
+        print(errorWithSignature.rstrip('\n'))
+
+    return issueCounter
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
diff --git a/checkstyle/src/main/resources/copyright-template.txt b/checkstyle/src/main/resources/copyright-template.txt
new file mode 100644 (file)
index 0000000..205e0ca
--- /dev/null
@@ -0,0 +1,16 @@
+============LICENSE_START=======================================================
+================================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+SPDX-License-Identifier: Apache-2.0
+============LICENSE_END=========================================================
diff --git a/checkstyle/src/main/resources/ignore-files-config.csv b/checkstyle/src/main/resources/ignore-files-config.csv
new file mode 100644 (file)
index 0000000..4f7394f
--- /dev/null
@@ -0,0 +1,6 @@
+file path
+*checkstyle/*
+*.json
+*.yang
+*.rst
+*.csv
\ No newline at end of file
diff --git a/checkstyle/src/main/resources/project-committers-config.csv b/checkstyle/src/main/resources/project-committers-config.csv
new file mode 100644 (file)
index 0000000..85ee43b
--- /dev/null
@@ -0,0 +1,3 @@
+email,signature
+@est.tech,Nordix Foundation
+@bell.ca,Bell Canada
\ No newline at end of file
diff --git a/checkstyle/src/main/test_CopyrightCheck.py b/checkstyle/src/main/test_CopyrightCheck.py
new file mode 100644 (file)
index 0000000..177f9d4
--- /dev/null
@@ -0,0 +1,441 @@
+#  ============LICENSE_START=======================================================
+#  Copyright (C) 2022 Nordix Foundation
+#  ================================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+#  SPDX-License-Identifier: Apache-2.0
+#  ============LICENSE_END=========================================================
+
+import datetime
+import sys
+import unittest
+from unittest import mock
+from unittest.mock import MagicMock
+import io
+
+import CopyrightCheck
+
+BANNER = '=' * 120
+
+def MockStdout(command):
+    mock_stdout = MagicMock()
+    mock_stdout.configure_mock(**{"stdout.decode.return_value": command})
+    return mock_stdout
+
+class TestCopyrightCheck(unittest.TestCase):
+
+    @mock.patch('subprocess.run')
+    def test_PermissionsCheckFalse(self, mock_subprocess_run):
+        mock_subprocess_run.return_value = MockStdout('Permission denied')
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with self.assertRaises(SystemExit):
+            CopyrightCheck.PermissionsCheck()
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(),
+                         'Error, I may not have the necessary permissions. Exiting...\n' + BANNER + '\n')
+
+    @mock.patch('subprocess.run')
+    def test_PermissionsCheckTrue(self, mock_subprocess_run):
+        mock_subprocess_run.return_value = MockStdout(
+            'usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]...')
+        CopyrightCheck.PermissionsCheck()   # Assert no error thrown
+
+    @mock.patch('CopyrightCheck.GetIgnoredFiles')
+    @mock.patch('subprocess.run')
+    def test_FindAlteredFiles(self, mock_subprocess_run, mock_GetIgnoredFiles):
+        mock_GetIgnoredFiles.return_value = ['.*.json', 'dir/.*']
+        mock_subprocess_run.return_value = MockStdout('File1.json\nFile2.java\nFile2.java\ndir/File3.java')
+        result = CopyrightCheck.FindAlteredFiles()
+        # Duplicates, .json and files in 'dir' removed
+        self.assertEqual(result, ['File2.java'])
+
+    @mock.patch('CopyrightCheck.GetIgnoredFiles')
+    @mock.patch('subprocess.run')
+    def test_FindAlteredFilesWithNoFileChanges(self, mock_subprocess_run, mock_GetIgnoredFiles):
+        mock_GetIgnoredFiles.return_value = ['.*.json', 'dir/.*']
+        mock_subprocess_run.return_value = MockStdout('File1.json\ndir/File3.java')
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.FindAlteredFiles()
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(result, [])
+        self.assertEqual(capturedOutput.getvalue(), '')
+
+    @mock.patch('subprocess.run')
+    def test_GetCommitterEmailExtension(self, mock_subprocess_run):
+        mock_subprocess_run.return_value = MockStdout('a.committer.name@address.com')
+        result = CopyrightCheck.GetCommitterEmailExtension()
+        self.assertEqual(result, '@address.com')
+
+    def test_ReadProjectCommittersConfigFile(self):
+        mock_open = mock.mock_open(read_data="email,signature\n@address.com,Company Name")
+        with mock.patch('builtins.open', mock_open):
+            result = CopyrightCheck.ReadProjectCommittersConfigFile()
+        self.assertEqual(result, {'@address.com': 'Company Name'})
+
+    @mock.patch('CopyrightCheck.open')
+    def test_ReadProjectCommittersConfigFileError(self, mock_OpenFile):
+        mock_OpenFile.side_effect = FileNotFoundError
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with self.assertRaises(SystemExit):
+            CopyrightCheck.ReadProjectCommittersConfigFile()
+        sys.stdout = sys.__stdout__
+        expectedOutput = ('Unable to open Project Committers Config File, have the command line arguments been set?\n' +
+                          BANNER + '\n')
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+    def test_CheckCommitterInConfigFileTrue(self):
+        committerEmailExtension = '@address.com'
+        projectCommitters = {'@address.com': 'Company Name'}
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCommitterInConfigFile(committerEmailExtension, projectCommitters)
+        sys.stdout = sys.__stdout__
+        self.assertTrue(result)
+        self.assertEqual(capturedOutput.getvalue(), "")
+
+    def test_CheckCommitterInConfigFileFalse(self):
+        committerEmailExtension = '@address.com'
+        projectCommitters = {'@anotheraddress.com': 'Another Company Name'}
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with self.assertRaises(SystemExit):
+            CopyrightCheck.CheckCommitterInConfigFile(committerEmailExtension, projectCommitters)
+        sys.stdout = sys.__stdout__
+        expectedOutput = ('Error, Committer email is not included in config file.\n' +
+                          'If your company is new to the project please make appropriate changes to project-committers-config.csv\n' +
+                          'for Copyright Check to work.\n' +
+                          'Exiting...\n' + BANNER + '\n')
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+    def test_GetIgnoredFiles(self):
+        mock_open = mock.mock_open(read_data="file path\n*checkstyle/*\n*.json")
+        with mock.patch('builtins.open', mock_open):
+            result = CopyrightCheck.GetIgnoredFiles()
+        self.assertEqual(result, [".*checkstyle/.*", ".*.json"])
+
+    @mock.patch('CopyrightCheck.open')
+    def test_GetIgnoredFilesError(self, mock_OpenFile):
+        mock_OpenFile.side_effect = FileNotFoundError
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with self.assertRaises(SystemExit):
+            CopyrightCheck.GetIgnoredFiles()
+        sys.stdout = sys.__stdout__
+        expectedOutput = ('Unable to open File Ignore Config File, have the command line arguments been set?\n' +
+                          BANNER + '\n')
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+    def test_GetCopyrightTemplate(self):
+        mock_open = mock.mock_open(read_data="****\nThis is a\nCopyright File\n****")
+        with mock.patch('builtins.open', mock_open):
+            result = CopyrightCheck.GetCopyrightTemplate()
+        self.assertEqual(result, ["****\n", "This is a\n", "Copyright File\n", "****"])
+
+    @mock.patch('CopyrightCheck.open')
+    def test_GetCopyrightTemplateError(self, mock_OpenFile):
+        mock_OpenFile.side_effect = FileNotFoundError
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with self.assertRaises(SystemExit):
+            CopyrightCheck.GetCopyrightTemplate()
+        sys.stdout = sys.__stdout__
+        expectedOutput = ('Unable to open Template Copyright File, have the command line arguments been set?\n' +
+                          BANNER + '\n')
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+    @mock.patch('subprocess.run')
+    def test_GetProjectRootDir(self, mock_subprocess_run):
+        mock_subprocess_run.return_value = MockStdout('project/root/dir\n')
+        result = CopyrightCheck.GetProjectRootDir()
+        self.assertEqual(result, 'project/root/dir/')
+
+
+    def test_ParseFileCopyright(self):
+        readFromFile = ["#Before lines will not be included\n",
+                        "#===LICENSE_START===\n",
+                        "#Copyright (C) 0000 Some Company\n",
+                        "#A line without signature\n",
+                        "#===============================\n",
+                        "#This is the start of the Copyright\n",
+                        "#===LICENSE_END===\n",
+                        "After lines will not be included"]
+        copyright, signatures = CopyrightCheck.ParseFileCopyright(readFromFile)
+        self.assertEqual(copyright, {2: "#===LICENSE_START===\n",
+                                     5: "#===============================\n",
+                                     6: "#This is the start of the Copyright\n",
+                                     7: "#===LICENSE_END===\n"})
+        self.assertEqual(signatures, {3: "#Copyright (C) 0000 Some Company\n",
+                                      4: "#A line without signature\n"})
+
+    def test_ParseFileCopyrightNoCopyright(self):
+        fileObject = io.StringIO("#This is not\na copyright\n")
+        fileObject.name = 'some/file/name'
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        copyright, signatures = CopyrightCheck.ParseFileCopyright(fileObject)
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(copyright, {})
+        self.assertEqual(signatures, {})
+        self.assertEqual(capturedOutput.getvalue(), 'some/file/name | no copyright found\n')
+
+    def test_RemoveCommentBlock(self):
+        commentCharactersList = ['# ', '* ', '#  ', '*  ']
+
+        for commentCharacters in commentCharactersList:
+            copyright = {1: commentCharacters + '===LICENSE_START===\n',
+                         2: '\n',
+                         3: commentCharacters + 'This is the License\n',
+                         4: commentCharacters + '===LICENSE_END===\n'}
+            result = CopyrightCheck.RemoveCommentBlock(copyright)
+            self.assertEqual(result, {1: '===LICENSE_START===\n',
+                                      2: '\n',
+                                      3: 'This is the License\n',
+                                      4: '===LICENSE_END===\n'})
+
+    @mock.patch('CopyrightCheck.open')
+    @mock.patch('CopyrightCheck.GetProjectRootDir')
+    @mock.patch('CopyrightCheck.GetCopyrightTemplate')
+    def test_CheckCopyrightForFileNotFound(self, mock_GetCopyrightTemplate, mock_GetProjectRootDir, mock_OpenFile):
+        mock_GetCopyrightTemplate.return_value = 'some-copyright-template'
+        mock_GetProjectRootDir.return_value = 'some/project/root/dir/'
+        mock_OpenFile.side_effect = FileNotFoundError
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightForFiles(['some-file.java'], {}, [])
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(), 'Unable to find file some/project/root/dir/some-file.java\n')
+        self.assertEqual(result, 1)
+
+    @mock.patch('CopyrightCheck.ParseFileCopyright')
+    @mock.patch('CopyrightCheck.GetProjectRootDir')
+    @mock.patch('CopyrightCheck.GetCopyrightTemplate')
+    def test_CheckCopyrightForFileWithNoCopyright(self, mock_GetCopyrightTemplate, mock_GetProjectRootDir,
+                                                  mock_ParseFileCopyright):
+        mock_GetCopyrightTemplate.return_value = 'some-copyright-template'
+        mock_GetProjectRootDir.return_value = 'some/project/root/dir/'
+        mock_ParseFileCopyright.return_value = ({}, {})
+        mock_open = mock.mock_open(read_data="some-file-content")
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with mock.patch('builtins.open', mock_open):
+            result = CopyrightCheck.CheckCopyrightForFiles(['some-file.java'], {}, [])
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(), "")
+        self.assertEqual(result, 1)
+
+
+    @mock.patch('CopyrightCheck.CheckCopyrightSignature')
+    @mock.patch('CopyrightCheck.CheckCopyrightFormat')
+    @mock.patch('CopyrightCheck.ParseFileCopyright')
+    @mock.patch('CopyrightCheck.GetProjectRootDir')
+    @mock.patch('CopyrightCheck.GetCopyrightTemplate')
+    def test_CheckCopyrightForFilesWhichAreRight(self, mock_GetCopyrightTemplate, mock_GetProjectRootDir,
+                                                  mock_ParseFileCopyright, mock_CheckCopyrightFormat,
+                                                  mock_CheckCopyrightSignature):
+        mock_GetCopyrightTemplate.return_value = 'some-copyright-template'
+        mock_GetProjectRootDir.return_value = 'some/project/root/dir/'
+        mock_ParseFileCopyright.return_value = ({1: '# =some-copyright-line'}, {2: '# =some-signature-line'})
+        mock_open = mock.mock_open(read_data="# =some-file-content")
+        mock_CheckCopyrightFormat.return_value = 0
+        mock_CheckCopyrightSignature.return_value = 0
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        with mock.patch('builtins.open', mock_open):
+            result = CopyrightCheck.CheckCopyrightForFiles(['some-file.java', 'another-file.java'], {'@address.com': 'Some Company'}, '@address.com')
+        sys.stdout = sys.__stdout__
+        self.assertEqual(result, 0)
+        self.assertEqual(capturedOutput.getvalue(), "")
+
+        mock_GetCopyrightTemplate.assert_called_once_with()
+        mock_GetProjectRootDir.assert_called_once_with()
+        self.assertEqual(mock_ParseFileCopyright.call_count, 2)
+        mock_CheckCopyrightFormat.assert_has_calls([
+            mock.call({1: '=some-copyright-line'}, 'some-copyright-template', 'some/project/root/dir/some-file.java'),
+            mock.call({1: '=some-copyright-line'}, 'some-copyright-template', 'some/project/root/dir/another-file.java')
+        ])
+        mock_CheckCopyrightSignature.assert_has_calls([
+            mock.call({2: '# =some-signature-line'}, 'Some Company', 'some/project/root/dir/some-file.java'),
+            mock.call({2: '# =some-signature-line'}, 'Some Company', 'some/project/root/dir/another-file.java')
+        ])
+        self.assertEqual(mock_CheckCopyrightFormat.call_count, 2)
+        self.assertEqual(mock_CheckCopyrightSignature.call_count, 2)
+
+
+    def test_CheckCopyrightFormatWhichIsWrong(self):
+        fileCopyright = {1: '---LICENSE_START---\n',
+                         2: 'This is the license typo\n',
+                         3: '',
+                         4: '===license_end===\n'}
+        templateCopyright = ['===LICENSE_START===\n',
+                             'This is the license\n',
+                             '\n',
+                             '===LICENSE_END===\n']
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightFormat(fileCopyright, templateCopyright, 'some/file/path')
+        sys.stdout = sys.__stdout__
+
+        expectedOutput = ("some/file/path | line  1 read \t  '---LICENSE_START---\\n'\n" +
+                          "some/file/path | line  1 expected '===LICENSE_START===\\n'\n" +
+                          "some/file/path | line  2 read \t  'This is the license typo\\n'\n" +
+                          "some/file/path | line  2 expected 'This is the license\\n'\n" +
+                          "some/file/path | line  3 read \t  ''\n" +
+                          "some/file/path | line  3 expected '\\n'\n" +
+                          "some/file/path | line  4 read \t  '===license_end===\\n'\n" +
+                          "some/file/path | line  4 expected '===LICENSE_END===\\n'\n")
+
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+        self.assertEqual(result, 4)
+
+    def test_CheckCopyrightFormatWhichIsCorrect(self):
+        fileCopyright = {1: '===LICENSE_START===\n',
+                         2: 'This is the license\n',
+                         3: '\n',
+                         4: '===LICENSE_END===\n'}
+        templateCopyright = ['===LICENSE_START===\n',
+                             'This is the license\n',
+                             '\n',
+                             '===LICENSE_END===\n']
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightFormat(fileCopyright, templateCopyright, 'some/file/path')
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(), "")
+        self.assertEqual(result, 0)
+
+    def test_CheckCopyrightSignatureWhichIsWrong(self):
+        fileSignatures = {1: "Trigger expected Copy-right",
+                          2: "Trigger expected Mod Copy-right"}
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightSignature(fileSignatures, 'Some-Company', 'some/file/path')
+        sys.stdout = sys.__stdout__
+
+        expectedOutput = ("some/file/path | line 1 expected Copyright\n" +
+                          "some/file/path | line 2 expected Modifications Copyright\n" +
+                          "some/file/path | missing company name and year for Some-Company\n")
+
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+        self.assertEqual(result, 3)
+
+    def test_CheckCopyrightSignatureWhichHasWrongYear(self):
+        currentYear = datetime.date.today().year
+        fileSignatures = {1: "Copyright (C) 1999 Some-Company"}
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightSignature(fileSignatures, 'Some-Company', 'some/file/path')
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(),
+                         "some/file/path | line 1 update year to include " + str(currentYear) + "\n")
+        self.assertEqual(result, 1)
+
+    def test_CheckCopyrightSignatureWhichIsRight(self):
+        currentYear = datetime.date.today().year
+        fileSignatures = {1: "Copyright (C) " + str(currentYear) + " Some-Company"}
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+        result = CopyrightCheck.CheckCopyrightSignature(fileSignatures, 'Some-Company', 'some/file/path')
+        sys.stdout = sys.__stdout__
+
+        self.assertEqual(capturedOutput.getvalue(), "")
+        self.assertEqual(result, 0)
+
+    @mock.patch('CopyrightCheck.CheckCopyrightForFiles')
+    @mock.patch('CopyrightCheck.FindAlteredFiles')
+    @mock.patch('CopyrightCheck.CheckCommitterInConfigFile')
+    @mock.patch('CopyrightCheck.ReadProjectCommittersConfigFile')
+    @mock.patch('CopyrightCheck.GetCommitterEmailExtension')
+    @mock.patch('CopyrightCheck.PermissionsCheck')
+    def test_Main(self, mock_PermissionsCheck, mock_GetCommitterEmailExtension, mock_ReadProjectCommittersConfigFile,
+                  mock_CheckCommitterInConfigFile, mock_FindAlteredFiles, mock_CheckCopyrightForFiles):
+
+        mock_GetCommitterEmailExtension.return_value = '@address.com'
+        mock_ReadProjectCommittersConfigFile.return_value = {'@address.com', 'Some Company'}
+        mock_FindAlteredFiles.return_value = ['some-file.java']
+        mock_CheckCopyrightForFiles.return_value = 5
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+
+        CopyrightCheck.main()
+
+        sys.stdout = sys.__stdout__
+
+        expectedOutput = (BANNER + '\nCopyright Check Python Script:\n' +
+                          '5 issue(s) found after 1 altered file(s) checked\n' +
+                          BANNER + '\n')
+
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+        mock_PermissionsCheck.assert_called_once_with()
+        mock_GetCommitterEmailExtension.assert_called_once_with()
+        mock_ReadProjectCommittersConfigFile.assert_called_once_with()
+        mock_CheckCommitterInConfigFile.assert_called_once_with('@address.com', {'@address.com', 'Some Company'})
+        mock_FindAlteredFiles.assert_called_once_with()
+        mock_CheckCopyrightForFiles.assert_called_once_with(['some-file.java'], {'@address.com', 'Some Company'}, '@address.com')
+
+    @mock.patch('CopyrightCheck.CheckCopyrightForFiles')
+    @mock.patch('CopyrightCheck.FindAlteredFiles')
+    @mock.patch('CopyrightCheck.CheckCommitterInConfigFile')
+    @mock.patch('CopyrightCheck.ReadProjectCommittersConfigFile')
+    @mock.patch('CopyrightCheck.GetCommitterEmailExtension')
+    @mock.patch('CopyrightCheck.PermissionsCheck')
+    def test_MainNoFiles(self, mock_PermissionsCheck, mock_GetCommitterEmailExtension, mock_ReadProjectCommittersConfigFile,
+                  mock_CheckCommitterInConfigFile, mock_FindAlteredFiles, mock_CheckCopyrightForFiles):
+
+        mock_GetCommitterEmailExtension.return_value = '@address.com'
+        mock_ReadProjectCommittersConfigFile.return_value = {'@address.com', 'Some Company'}
+        mock_FindAlteredFiles.return_value = []
+
+        capturedOutput = io.StringIO()
+        sys.stdout = capturedOutput  # Capture output to stdout
+
+        CopyrightCheck.main()
+
+        sys.stdout = sys.__stdout__
+
+        expectedOutput = (BANNER + '\nCopyright Check Python Script:\n' +
+                          '0 issue(s) found after 0 altered file(s) checked\n' +
+                          BANNER + '\n')
+
+        self.assertEqual(capturedOutput.getvalue(), expectedOutput)
+
+        mock_PermissionsCheck.assert_called_once_with()
+        mock_GetCommitterEmailExtension.assert_called_once_with()
+        mock_ReadProjectCommittersConfigFile.assert_called_once_with()
+        mock_CheckCommitterInConfigFile.assert_called_once_with('@address.com', {'@address.com', 'Some Company'})
+        mock_FindAlteredFiles.assert_called_once_with()
+        mock_CheckCopyrightForFiles.assert_not_called()
+
+
+if __name__ == '__main__':
+    unittest.main()
index 50b06b2..193599f 100755 (executable)
@@ -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>
 
index 3e5f70d..e468926 100644 (file)
@@ -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>
         <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>
index f04213d..dcbc5f7 100755 (executable)
@@ -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.
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
+
+  SPDX-License-Identifier: Apache-2.0
   ============LICENSE_END=========================================================
 -->
 <project xmlns="http://maven.apache.org/POM/4.0.0"
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.onap.cps</groupId>
     <artifactId>cps-dependencies</artifactId>
-    <version>3.0.0-SNAPSHOT</version>
+    <version>3.1.0-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <name>${project.groupId}:${project.artifactId}</name>
         <mapstruct.version>1.4.2.Final</mapstruct.version>
     </properties>
 
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-deploy-plugin</artifactId>
+                    <version>2.8.2</version>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+
     <distributionManagement>
         <repository>
             <id>ecomp-releases</id>
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-dependencies</artifactId>
-                <version>2.5.5</version>
+                <version>2.6.4</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
             <dependency>
                 <groupId>org.springframework.cloud</groupId>
                 <artifactId>spring-cloud-dependencies</artifactId>
-                <version>2020.0.2</version>
+                <version>2021.0.1</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
-            <dependency>
-                <groupId>org.springframework</groupId>
-                <artifactId>spring-web</artifactId>
-                <version>5.3.13</version>
-            </dependency>
             <dependency>
                 <groupId>org.opendaylight.yangtools</groupId>
                 <artifactId>yangtools-artifacts</artifactId>
                 <version>0.18.0</version>
                 <scope>test</scope>
             </dependency>
-            <dependency>
-                <groupId>org.apache.logging.log4j</groupId>
-                <artifactId>log4j-api</artifactId>
-                <version>2.17.1</version>
-            </dependency>
-            <dependency>
-                <groupId>org.apache.logging.log4j</groupId>
-                <artifactId>log4j-to-slf4j</artifactId>
-                <version>2.17.1</version>
-            </dependency>
             <dependency>
                 <groupId>org.mapstruct</groupId>
                 <artifactId>mapstruct</artifactId>
index b9b399c..9bd9588 100644 (file)
@@ -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>
 
index 69225ae..7ed2efe 100644 (file)
@@ -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"
index 3cd8e8b..0a408c2 100755 (executable)
@@ -31,7 +31,7 @@ updateDmiRegistration:
           schema:
             $ref: 'components.yaml#/components/schemas/RestDmiPluginRegistration'
     responses:
-      204:
+      200:
         $ref: 'components.yaml#/components/responses/NoContent'
       400:
         $ref: 'components.yaml#/components/responses/BadRequest'
@@ -40,4 +40,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"
+                }
+              ]
index a9d08b7..05e4b84 100755 (executable)
@@ -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:
index 12a8318..935b657 100755 (executable)
@@ -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'
index 97305cf..6a700c3 100644 (file)
@@ -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>
 
index 4c8fafe..a9ec863 100644 (file)
@@ -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);
index 0201fad..5c1f870 100755 (executable)
@@ -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
@@ -194,6 +215,19 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
         return ResponseEntity.ok(cmHandles);
     }
 
+    /**
+     * Query and return cm handles that match the given query parameters.
+     *
+     * @param cmHandleQueryRestParameters the cm handle query parameters
+     * @return collection of cm handle ids
+     */
+    public ResponseEntity<List<String>> queryCmHandles(
+        final CmHandleQueryRestParameters cmHandleQueryRestParameters) {
+        final Set<String> cmHandleIds = networkCmProxyDataService.queryCmHandles(
+            jsonObjectMapper.convertToValueType(cmHandleQueryRestParameters, CmHandleQueryApiParameters.class));
+        return ResponseEntity.ok(List.copyOf(cmHandleIds));
+    }
+
     /**
      * Search for Cm Handle and Properties by Name.
      * @param cmHandleId cm-handle identifier
@@ -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;
+    }
+
 }
index c9d26f2..105a6a5 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021 Bell Canada
+ *  Copyright (C) 2021-2022 Bell Canada
  *  Modifications Copyright (C) 2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
 
 package org.onap.cps.ncmp.rest.controller;
 
+import java.util.List;
+import java.util.stream.Collectors;
 import javax.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse;
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status;
+import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse;
 import org.onap.cps.ncmp.rest.api.NetworkCmProxyInventoryApi;
+import org.onap.cps.ncmp.rest.model.CmHandlerRegistrationErrorResponse;
+import org.onap.cps.ncmp.rest.model.DmiPluginRegistrationErrorResponse;
 import org.onap.cps.ncmp.rest.model.RestDmiPluginRegistration;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -41,14 +48,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());
     }
 
 }
index 0843e97..c723733 100755 (executable)
@@ -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);
+    }
 }
index 3d54a0b..bb76208 100644 (file)
@@ -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
index d5c3cd9..b34b0ff 100644 (file)
@@ -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'
+    }
+
 }
 
index 9b1c2e8..30b6beb 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021 Bell Canada
+ *  Copyright (C) 2021-2022 Bell Canada
  *  Modifications Copyright (C) 2021-2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,7 +24,11 @@ package org.onap.cps.ncmp.rest.controller
 import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.TestUtils
 import org.onap.cps.ncmp.api.NetworkCmProxyDataService
+import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse
 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
+import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse
+import org.onap.cps.ncmp.rest.model.CmHandlerRegistrationErrorResponse
+import org.onap.cps.ncmp.rest.model.DmiPluginRegistrationErrorResponse
 import org.onap.cps.ncmp.rest.model.RestDmiPluginRegistration
 import org.onap.cps.utils.JsonObjectMapper
 import org.spockframework.spring.SpringBean
@@ -36,6 +40,9 @@ import org.springframework.http.HttpStatus
 import org.springframework.http.MediaType
 import org.springframework.test.web.servlet.MockMvc
 import spock.lang.Specification
+
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_ALREADY_EXIST
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_DOES_NOT_EXIST
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
 
 @WebMvcTest(NetworkCmProxyInventoryController)
@@ -58,7 +65,7 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
     @Value('${rest.api.ncmp-inventory-base-path}/v1')
     def ncmpBasePathV1
 
-    def 'Dmi plugin registration #scenario' () {
+    def 'Dmi plugin registration #scenario'() {
         given: 'a dmi plugin registration with #scenario'
             def jsonData = TestUtils.getResourceFileContent(dmiRegistrationJson)
         and: 'the expected rest input as an object'
@@ -72,9 +79,9 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
                     .content(jsonData)
             ).andReturn().response
         then: 'the converted object is forwarded to the registration service'
-            1 * mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(mockDmiPluginRegistration)
+            1 * mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(mockDmiPluginRegistration) >> new DmiPluginRegistrationResponse()
         and: 'response status is no content'
-            response.status ==  HttpStatus.NO_CONTENT.value()
+            response.status == HttpStatus.OK.value()
         where: 'the following registration json is used'
             scenario                                                                       | dmiRegistrationJson
             'multiple services, added, updated and removed cm handles and many properties' | 'dmi_registration_all_singing_and_dancing.json'
@@ -82,7 +89,7 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
             'without any properties'                                                       | 'dmi_registration_without_properties.json'
     }
 
-    def 'Dmi plugin registration with invalid json' () {
+    def 'Dmi plugin registration with invalid json'() {
         given: 'a dmi plugin registration with #scenario'
             def jsonDataWithUndefinedDataLabel = '{"notAdmiPlugin":""}'
         when: 'post request is performed & registration is called with correct DMI plugin information'
@@ -95,4 +102,74 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
             response.status == HttpStatus.BAD_REQUEST.value()
     }
 
+    def 'DMI Registration: All cm-handles operations processed successfully.'() {
+        given: 'a dmi plugin registration'
+            def dmiRegistrationRequest = '{}'
+        and: 'service can register cm-handles successfully'
+            def dmiRegistrationResponse = new DmiPluginRegistrationResponse(
+                createdCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-1')],
+                updatedCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-2')],
+                removedCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-3')]
+            )
+            mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(*_) >> dmiRegistrationResponse
+        when: 'registration endpoint is invoked'
+            def response = mvc.perform(
+                post("$ncmpBasePathV1/ch")
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .content(dmiRegistrationRequest)
+            ).andReturn().response
+        then: 'response status is ok'
+            response.status == HttpStatus.OK.value()
+        and: 'the response body is empty'
+            response.getContentAsString() == ''
+
+    }
+
+    def 'DMI Registration Error Handling: #scenario.'() {
+        given: 'a dmi plugin registration'
+            def dmiRegistrationRequest = '{}'
+        and: '#scenario: service failed to register few cm-handle'
+            def dmiRegistrationResponse = new DmiPluginRegistrationResponse(
+                createdCmHandles: [createCmHandleResponse],
+                updatedCmHandles: [updateCmHandleResponse],
+                removedCmHandles: [removeCmHandleResponse]
+            )
+            mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(*_) >> dmiRegistrationResponse
+        when: 'registration endpoint is invoked'
+            def response = mvc.perform(
+                post("$ncmpBasePathV1/ch")
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .content(dmiRegistrationRequest)
+            ).andReturn().response
+        then: 'request status is internal server error'
+            response.status == HttpStatus.INTERNAL_SERVER_ERROR.value()
+        and: 'the response body is in the expected format'
+            def responseBody = jsonObjectMapper.convertJsonString(response.getContentAsString(), DmiPluginRegistrationErrorResponse)
+        and: 'contains only the failure responses'
+            responseBody.getFailedCreatedCmHandles() == expectedFailedCreatedCmHandle
+            responseBody.getFailedUpdatedCmHandles() == expectedFailedUpdateCmHandle
+            responseBody.getFailedRemovedCmHandles() == expectedFailedRemovedCmHandle
+        where:
+            scenario               | createCmHandleResponse         | updateCmHandleResponse         | removeCmHandleResponse         || expectedFailedCreatedCmHandle       | expectedFailedUpdateCmHandle        | expectedFailedRemovedCmHandle
+            'only create failed'   | failedResponse('cm-handle-1')  | successResponse('cm-handle-2') | successResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | []                                  | []
+            'only update failed'   | successResponse('cm-handle-1') | failedResponse('cm-handle-2')  | successResponse('cm-handle-3') || []                                  | [failedRestResponse('cm-handle-2')] | []
+            'only delete failed'   | successResponse('cm-handle-1') | successResponse('cm-handle-2') | failedResponse('cm-handle-3')  || []                                  | []                                  | [failedRestResponse('cm-handle-3')]
+            'all three failed'     | failedResponse('cm-handle-1')  | failedResponse('cm-handle-2')  | failedResponse('cm-handle-3')  || [failedRestResponse('cm-handle-1')] | [failedRestResponse('cm-handle-2')] | [failedRestResponse('cm-handle-3')]
+            'create update failed' | failedResponse('cm-handle-1')  | failedResponse('cm-handle-2')  | successResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | [failedRestResponse('cm-handle-2')] | []
+            'create delete failed' | failedResponse('cm-handle-1')  | successResponse('cm-handle-2') | failedResponse('cm-handle-3')  || [failedRestResponse('cm-handle-1')] | []                                  | [failedRestResponse('cm-handle-3')]
+            'update delete failed' | successResponse('cm-handle-1') | failedResponse('cm-handle-2')  | failedResponse('cm-handle-3')  || []                                  | [failedRestResponse('cm-handle-2')] | [failedRestResponse('cm-handle-3')]
+    }
+
+    def failedRestResponse(cmHandle) {
+        return new CmHandlerRegistrationErrorResponse('cmHandle': cmHandle, 'errorCode': '00', 'errorText': 'Failed')
+    }
+
+    def failedResponse(cmHandle) {
+        return CmHandleRegistrationResponse.createFailureResponse(cmHandle, new RuntimeException("Failed"))
+    }
+
+    def successResponse(cmHandle) {
+        return CmHandleRegistrationResponse.createSuccessResponse(cmHandle)
+    }
+
 }
index b642370..1f6c384 100644 (file)
 
 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 }
index fd8b56b..c2a307d 100644 (file)
@@ -3,7 +3,7 @@
   "dmiModelPlugin":"service3",
   "createdCmHandles":[
     {
-      "cmHandle":"ch1(new)",
+      "cmHandle":"ch1-new",
       "cmHandleProperties":{
         "dmiProp1":"ch1-dmi1",
         "dmiProp2":"ch1-dmi2"
@@ -14,7 +14,7 @@
       }
     },
     {
-      "cmHandle":"ch2(new)",
+      "cmHandle":"ch2-new",
       "cmHandleProperties":{
         "dmiProp1":"ch2-dmi1",
         "dmiProp2":"ch2-dmi2"
@@ -27,7 +27,7 @@
   ],
   "updatedCmHandles":[
     {
-      "cmHandle":"ch3(upd)",
+      "cmHandle":"ch3-upd",
       "cmHandleProperties":{
         "dmiProp1":"ch3-dmi1"
       },
index 58a1a98..26acdbd 100644 (file)
@@ -2,7 +2,7 @@
   "dmiPlugin": "service1",
   "updatedCmHandles":[
     {
-      "cmHandle":"ch3(upd)",
+      "cmHandle":"ch3-upd",
       "cmHandleProperties":{
         "dmiProp1":"ch3-dmi1",
         "dmiProp2":null
index 395c098..a5dd7b0 100644 (file)
@@ -4,7 +4,7 @@
   "dmiModelPlugin":"service3",
   "createdCmHandles":[
     {
-      "cmHandle": "ch1(new)"
+      "cmHandle": "ch1-new"
     }
   ]
 }
index fe061ea..573c76e 100644 (file)
@@ -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>
 
index d942d26..058c42b 100644 (file)
@@ -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);
 }
index 76d4cef..e624953 100755 (executable)
@@ -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
index ca2f578..aae2f20 100644 (file)
@@ -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) {
index 94faa55..f1bb95f 100644 (file)
@@ -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.
 
 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 (file)
index 0000000..9d307e5
--- /dev/null
@@ -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;
+    }
+}
index 68de9d5..ad85edd 100644 (file)
@@ -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);
     }
 
 }
index d79988e..b033af8 100644 (file)
@@ -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,
index 75ba91b..745007b 100644 (file)
@@ -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;
-    }
 
 }
index 6b6bdf5..0efe8d5 100644 (file)
@@ -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")),
index b60aac9..b679107 100644 (file)
@@ -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;
     }
index 47062b3..fd35281 100644 (file)
@@ -21,6 +21,8 @@
 
 package org.onap.cps.ncmp.api.impl.yangmodels;
 
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.Strings;
 import java.util.ArrayList;
@@ -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 (file)
index 261a018..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2022 Nordix Foundation
- *  ================================================================================
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- *
- *  SPDX-License-Identifier: Apache-2.0
- *  ============LICENSE_END=========================================================
- */
-
-package org.onap.cps.ncmp.api.impl.yangmodels;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import lombok.Getter;
-import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
-
-@Getter
-public class YangModelCmHandlesList {
-
-    @JsonProperty("cm-handles")
-    private final List<YangModelCmHandle> yangModelCmHandles = new ArrayList<>();
-
-    /**
-     * Create a YangModelCmHandleList given all service names and a collection of cmHandles.
-     * @param dmiServiceName the dmi service name
-     * @param dmiDataServiceName the dmi data service name
-     * @param dmiModelServiceName the dmi model service name
-     * @param ncmpServiceCmHandles cm handles rest model
-     * @return instance of YangModelCmHandleList
-     */
-    public static YangModelCmHandlesList toYangModelCmHandlesList(final String dmiServiceName,
-                                                                  final String dmiDataServiceName,
-                                                                  final String dmiModelServiceName,
-                                                                  final Collection<NcmpServiceCmHandle>
-                                                            ncmpServiceCmHandles) {
-        final YangModelCmHandlesList yangModelCmHandlesList = new YangModelCmHandlesList();
-        for (final NcmpServiceCmHandle ncmpServiceCmHandle : ncmpServiceCmHandles) {
-            final YangModelCmHandle yangModelCmHandle =
-                YangModelCmHandle.toYangModelCmHandle(
-                    dmiServiceName,
-                    dmiDataServiceName,
-                    dmiModelServiceName,
-                    ncmpServiceCmHandle);
-            yangModelCmHandlesList.add(yangModelCmHandle);
-        }
-        return yangModelCmHandlesList;
-    }
-
-    /**
-     * Add a yangModelCmHandle.
-     *
-     * @param yangModelCmHandle the yangModelCmHandle to add
-     */
-    public void add(final YangModelCmHandle yangModelCmHandle) {
-        yangModelCmHandles.add(yangModelCmHandle);
-    }
-}
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 (file)
index 0000000..1d00f0d
--- /dev/null
@@ -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 (file)
index 0000000..3f584ed
--- /dev/null
@@ -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 (file)
index 0000000..1da2aa9
--- /dev/null
@@ -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 (file)
index 0000000..8a3d264
--- /dev/null
@@ -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
index 9381270..6811b59 100644 (file)
@@ -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();
index e410463..5683d57 100644 (file)
@@ -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))
+    }
 }
index c21d7e7..7629500 100644 (file)
@@ -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.
 
 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')
     }
 }
index 9b8d4ad..5eba5ee 100644 (file)
@@ -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.
 
 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
     }
+
 }
index 389086c..394df1d 100644 (file)
@@ -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
     }
index 3df862a..2a19df1 100644 (file)
@@ -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'
index d3fc17c..574f609 100644 (file)
@@ -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'
index e6f63ce..563116f 100644 (file)
@@ -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) {
index 593a6ec..bc30c9c 100644 (file)
@@ -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
+    }
 }
@@ -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.
  *  ============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 (file)
index 0000000..4476998
--- /dev/null
@@ -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
+    }
+
+}
index 470015e..7bbc3d7 100644 (file)
@@ -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:
index 1615d05..4c8dcac 100644 (file)
@@ -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'
index e03dce3..b76c63c 100755 (executable)
@@ -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>
                 <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>
index c8b88e8..1b0ebe2 100644 (file)
@@ -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>
index cefeac4..40ad410 100644 (file)
@@ -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
index ebf6fd3..21f5173 100644 (file)
@@ -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("'");
+    }
 }
index de7adf2..53490f3 100644 (file)
@@ -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 (file)
index 0000000..97d7d1d
--- /dev/null
@@ -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 (executable)
index 0000000..4a67167
--- /dev/null
@@ -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;
+    }
+}
index bfec574..b837a64 100644 (file)
@@ -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
     }
-
 }
index a25f81e..5852c0c 100644 (file)
@@ -29,6 +29,8 @@ dataspaces:
     responses:
       '201':
         $ref: 'components.yml#/components/responses/Created'
+      '400':
+        $ref: 'components.yml#/components/responses/BadRequest'
       '401':
         $ref: 'components.yml#/components/responses/Unauthorized'
       '403':
index 5a21957..6019197 100755 (executable)
@@ -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>
 
index 58a5ebf..41ad9ca 100755 (executable)
 
 package org.onap.cps.rest.controller
 
-import org.mapstruct.factory.Mappers
-
 import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
 
+import org.mapstruct.factory.Mappers
 import org.onap.cps.api.CpsAdminService
 import org.onap.cps.api.CpsModuleService
 import org.onap.cps.spi.exceptions.AlreadyDefinedException
@@ -73,7 +72,7 @@ class AdminRestControllerSpec extends Specification {
 
     def 'Create new dataspace.'() {
         given: 'an endpoint'
-            def createDataspaceEndpoint = "$basePath/v1/dataspaces";
+            def createDataspaceEndpoint = "$basePath/v1/dataspaces"
         when: 'post is invoked'
             def response =
                     mvc.perform(
@@ -88,7 +87,7 @@ class AdminRestControllerSpec extends Specification {
 
     def 'Create dataspace over existing with same name.'() {
         given: 'an endpoint'
-            def createDataspaceEndpoint = "$basePath/v1/dataspaces";
+            def createDataspaceEndpoint = "$basePath/v1/dataspaces"
         and: 'the service method throws an exception indicating the dataspace is already defined'
             def thrownException = new AlreadyDefinedException(dataspaceName, new RuntimeException())
             mockCpsAdminService.createDataspace(dataspaceName) >> { throw thrownException }
index 2aa4ddd..d4c68c3 100644 (file)
@@ -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
index 6e92894..98a392a 100644 (file)
@@ -26,7 +26,7 @@
     <parent>\r
         <groupId>org.onap.cps</groupId>\r
         <artifactId>cps-parent</artifactId>\r
-        <version>3.0.0-SNAPSHOT</version>\r
+        <version>3.1.0-SNAPSHOT</version>\r
         <relativePath>../cps-parent/pom.xml</relativePath>\r
     </parent>\r
 \r
index 50b2720..2e7bb7e 100755 (executable)
@@ -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);
index 78862d7..daf4dd7 100644 (file)
@@ -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);
index 6551937..4bc9dd9 100644 (file)
@@ -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);
+
 }
index ce2bfe7..f70e218 100644 (file)
@@ -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);
index 0e79deb..f85dea3 100644 (file)
@@ -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 (file)
index 0000000..e278688
--- /dev/null
@@ -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 (file)
index 0000000..2bd7ac3
--- /dev/null
@@ -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 (file)
index 0000000..98e6cfc
--- /dev/null
@@ -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
index 063bd5b..2de087f 100644 (file)
@@ -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
+    }
 }
index ae88d30..36b378a 100644 (file)
@@ -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'              || []
     }
index ab29005..6f780fc 100755 (executable)
@@ -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])
@@ -207,6 +209,15 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             'empty xpath' | ''
     }
 
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Cps Path query with syntax error throws a CPS Path Exception.'() {
+        when: 'trying to execute a query with a syntax (parsing) error'
+            objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS)
+        then: 'exception is thrown'
+            def exceptionThrown = thrown(CpsPathException)
+            assert exceptionThrown.getDetails().contains('failed to parse at line 1 due to extraneous input \'invalid-cps-path\' expecting \'/\'')
+    }
+
     @Sql([CLEAR_DATA, SET_DATA])
     def 'Get data node by xpath with all descendants.'() {
         when: 'data node is requested with all descendants'
@@ -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])
index 7166008..b37f471 100644 (file)
@@ -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 (file)
index 0000000..9b58c8b
--- /dev/null
@@ -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 (file)
index 0000000..a2df06e
--- /dev/null
@@ -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')
+    }
+
+}
index 8f525df..d1a6220 100644 (file)
@@ -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"}');
index a27bb5f..4106541 100755 (executable)
@@ -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.
    ================================================================================
 */
 
 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 (file)
index 0000000..fae9275
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<!DOCTYPE hibernate-configuration PUBLIC\r
+        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"\r
+        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">\r
+\r
+<hibernate-configuration>\r
+    <session-factory>\r
+        <property name="hibernate.connection.driver_class">org.postgresql.Driver</property>\r
+        <property name="hibernate.connection.url">${DB_URL}</property>\r
+        <property name="hibernate.connection.username">${DB_USERNAME}</property>\r
+        <property name="hibernate.connection.password">${DB_PASSWORD}</property>\r
+        <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQL82Dialect</property>\r
+        <property name="show_sql">true</property>\r
+        <property name="hibernate.hbm2ddl.auto">none</property>\r
+    </session-factory>\r
+</hibernate-configuration>
\ No newline at end of file
index 9c7031e..aea122d 100644 (file)
@@ -28,7 +28,7 @@
   <parent>\r
     <groupId>org.onap.cps</groupId>\r
     <artifactId>cps-parent</artifactId>\r
-    <version>3.0.0-SNAPSHOT</version>\r
+    <version>3.1.0-SNAPSHOT</version>\r
     <relativePath>../cps-parent/pom.xml</relativePath>\r
   </parent>\r
 \r
index 44f7f77..2106f15 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020 Nordix Foundation
+ *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  ================================================================================
 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);
 }
index cdd417b..93c96ec 100644 (file)
@@ -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);
+
 }
index ecc9bf0..79d6e03 100644 (file)
@@ -23,7 +23,6 @@ package org.onap.cps.api;
 
 import java.util.Collection;
 import java.util.Map;
-import org.checkerframework.checker.nullness.qual.NonNull;
 import org.onap.cps.spi.CascadeDeleteAllowed;
 import org.onap.cps.spi.exceptions.DataInUseException;
 import org.onap.cps.spi.model.ModuleReference;
@@ -42,8 +41,8 @@ public interface CpsModuleService {
      * @param yangResourcesNameToContentMap yang resources (files) as a mep where key is resource name
      *                                      and value is content
      */
-    void createSchemaSet(@NonNull String dataspaceName, @NonNull String schemaSetName,
-                         @NonNull Map<String, String> yangResourcesNameToContentMap);
+    void createSchemaSet(String dataspaceName, String schemaSetName,
+                         Map<String, String> yangResourcesNameToContentMap);
 
     /**
      * Create a schema set from new modules and existing modules.
@@ -52,8 +51,8 @@ public interface CpsModuleService {
      * @param newModuleNameToContentMap YANG resources map where key is a module name and value is content
      * @param moduleReferences          List of YANG resources module references of the modules
      */
-    void createSchemaSetFromModules(@NonNull String dataspaceName, @NonNull String schemaSetName,
-                                    @NonNull Map<String, String> newModuleNameToContentMap,
+    void createSchemaSetFromModules(String dataspaceName, String schemaSetName,
+                                    Map<String, String> newModuleNameToContentMap,
                                     Collection<ModuleReference> moduleReferences);
 
     /**
@@ -63,7 +62,7 @@ public interface CpsModuleService {
      * @param schemaSetName schema set name
      * @return a SchemaSet
      */
-    SchemaSet getSchemaSet(@NonNull String dataspaceName, @NonNull String schemaSetName);
+    SchemaSet getSchemaSet(String dataspaceName, String schemaSetName);
 
     /**
      * Deletes Schema Set.
@@ -74,8 +73,8 @@ public interface CpsModuleService {
      * @throws DataInUseException if cascadeDeleteAllowed is set to CASCADE_DELETE_PROHIBITED and there
      *                           is associated anchor record exists in database
      */
-    void deleteSchemaSet(@NonNull String dataspaceName, @NonNull String schemaSetName,
-        @NonNull CascadeDeleteAllowed cascadeDeleteAllowed);
+    void deleteSchemaSet(String dataspaceName, String schemaSetName,
+                         CascadeDeleteAllowed cascadeDeleteAllowed);
 
     /**
      * Retrieve module references for the given dataspace name.
index beb0a15..68ae1eb 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020 Nordix Foundation
+ *  Copyright (C) 2020-2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -21,7 +21,6 @@
 package org.onap.cps.api;
 
 import java.util.Collection;
-import org.checkerframework.checker.nullness.qual.NonNull;
 import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.model.DataNode;
 
@@ -40,7 +39,7 @@ public interface CpsQueryService {
      *                               included in the output
      * @return a collection of data nodes
      */
-    Collection<DataNode> queryDataNodes(@NonNull String dataspaceName, @NonNull String anchorName,
-        @NonNull String cpsPath, @NonNull FetchDescendantsOption fetchDescendantsOption);
+    Collection<DataNode> queryDataNodes(String dataspaceName, String anchorName,
+                                        String cpsPath, FetchDescendantsOption fetchDescendantsOption);
 
 }
index 1013add..762754f 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020 Nordix Foundation
+ *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  ================================================================================
@@ -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);
+    }
 }
index aae355d..2f1067a 100755 (executable)
@@ -36,6 +36,7 @@ import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.spi.model.Anchor;
 import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.model.DataNodeBuilder;
+import org.onap.cps.utils.CpsValidator;
 import org.onap.cps.utils.YangUtils;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
@@ -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);
@@ -108,10 +116,32 @@ public class CpsDataServiceImpl implements CpsDataService {
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE);
     }
 
+    @Override
+    public String startSession() {
+        return cpsDataPersistenceService.startSession();
+    }
+
+    @Override
+    public void closeSession(final String sessionId) {
+        cpsDataPersistenceService.closeSession(sessionId);
+    }
+
+    @Override
+    public void lockAnchor(final String sessionID, final String dataspaceName, final String anchorName) {
+        lockAnchor(sessionID, dataspaceName, anchorName, DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS);
+    }
+
+    @Override
+    public void lockAnchor(final String sessionID, final String dataspaceName,
+                           final String anchorName, final Long timeoutInMilliseconds) {
+        cpsDataPersistenceService.lockAnchor(sessionID, dataspaceName, anchorName, timeoutInMilliseconds);
+    }
+
     @Override
     public void replaceNodeTree(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final String jsonData, final OffsetDateTime observedTimestamp) {
-        final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
+        final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
         cpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName, dataNode);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE);
     }
@@ -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);
     }
 
index f0e79c6..db8a81f 100644 (file)
@@ -33,6 +33,7 @@ import org.onap.cps.spi.exceptions.SchemaSetInUseException;
 import org.onap.cps.spi.model.Anchor;
 import org.onap.cps.spi.model.ModuleReference;
 import org.onap.cps.spi.model.SchemaSet;
+import org.onap.cps.utils.CpsValidator;
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -48,6 +49,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
     @Override
     public void createSchemaSet(final String dataspaceName, final String schemaSetName,
         final Map<String, String> yangResourcesNameToContentMap) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         final var yangTextSchemaSourceSet
             = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap);
         cpsModulePersistenceService.storeSchemaSet(dataspaceName, schemaSetName, yangResourcesNameToContentMap);
@@ -58,6 +60,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
     public void createSchemaSetFromModules(final String dataspaceName, final String schemaSetName,
         final Map<String, String> newModuleNameToContentMap,
         final Collection<ModuleReference> moduleReferences) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         cpsModulePersistenceService.storeSchemaSetFromModules(dataspaceName, schemaSetName,
             newModuleNameToContentMap, moduleReferences);
 
@@ -65,6 +68,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
 
     @Override
     public SchemaSet getSchemaSet(final String dataspaceName, final String schemaSetName) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         final var yangTextSchemaSourceSet = yangTextSchemaSourceSetCache
             .get(dataspaceName, schemaSetName);
         return SchemaSet.builder().name(schemaSetName).dataspaceName(dataspaceName)
@@ -75,6 +79,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
     @Transactional
     public void deleteSchemaSet(final String dataspaceName, final String schemaSetName,
         final CascadeDeleteAllowed cascadeDeleteAllowed) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         final Collection<Anchor> anchors = cpsAdminService.getAnchors(dataspaceName, schemaSetName);
         if (!anchors.isEmpty() && isCascadeDeleteProhibited(cascadeDeleteAllowed)) {
             throw new SchemaSetInUseException(dataspaceName, schemaSetName);
@@ -89,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;
+    }
+
 }
index dd9f160..c2003d6 100644 (file)
@@ -25,6 +25,7 @@ import org.onap.cps.api.CpsQueryService;
 import org.onap.cps.spi.CpsDataPersistenceService;
 import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.utils.CpsValidator;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -37,6 +38,7 @@ public class CpsQueryServiceImpl implements CpsQueryService {
     @Override
     public Collection<DataNode> queryDataNodes(final String dataspaceName, final String anchorName,
         final String cpsPath, final FetchDescendantsOption fetchDescendantsOption) {
+        CpsValidator.validateNameCharacters(dataspaceName, anchorName);
         return cpsDataPersistenceService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption);
     }
 }
index 03b52a3..fb881a9 100644 (file)
@@ -24,6 +24,7 @@ package org.onap.cps.api.impl;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.Map;
 import org.onap.cps.spi.CpsModulePersistenceService;
+import org.onap.cps.utils.CpsValidator;
 import org.onap.cps.yang.YangTextSchemaSourceSet;
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -52,6 +53,7 @@ public class YangTextSchemaSourceSetCache {
      */
     @Cacheable(key = "#p0.concat('-').concat(#p1)")
     public YangTextSchemaSourceSet get(final String dataspaceName, final String schemaSetName) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         final Map<String, String> yangResourceNameToContent =
                 cpsModulePersistenceService.getYangSchemaResources(dataspaceName, schemaSetName);
         return YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent);
@@ -69,6 +71,7 @@ public class YangTextSchemaSourceSetCache {
     @CanIgnoreReturnValue
     public YangTextSchemaSourceSet updateCache(final String dataspaceName, final String schemaSetName,
             final YangTextSchemaSourceSet yangTextSchemaSourceSet) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         return yangTextSchemaSourceSet;
     }
 
@@ -81,6 +84,7 @@ public class YangTextSchemaSourceSetCache {
      */
     @CacheEvict(key = "#p0.concat('-').concat(#p1)")
     public void removeFromCache(final String dataspaceName, final String schemaSetName) {
+        CpsValidator.validateNameCharacters(dataspaceName, schemaSetName);
         // Spring provides implementation for removing object from cache
     }
 
index dd4059d..25167e8 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  * ============LICENSE_START=======================================================
- *  Copyright (C) 2020 Nordix Foundation.
+ *  Copyright (C) 2020-2022 Nordix Foundation.
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  * ================================================================================
 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);
 }
index fd65886..43cfffe 100644 (file)
@@ -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 (file)
index 0000000..4000bfc
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.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 (file)
index 0000000..92b4aa7
--- /dev/null
@@ -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 (file)
index 0000000..ff4e627
--- /dev/null
@@ -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();
+
+}
index 55e7b99..43aa06b 100644 (file)
@@ -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 (file)
index 0000000..28b49c9
--- /dev/null
@@ -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();
+    }
+}
index bb122d1..33868cc 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020 Nordix Foundation
+ *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  ================================================================================
@@ -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(_)
+    }
+
 }
index 785788b..8b9d545 100644 (file)
@@ -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)
+    }
 }
index bae06bb..95d7314 100644 (file)
@@ -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')]
index 4878f4c..55a252c 100644 (file)
@@ -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'
+    }
+
 }
index 860b739..06c675a 100644 (file)
@@ -1,6 +1,7 @@
 /*
  * ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Bell Canada
+ *  Modifications Copyright (C) 2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -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 (file)
index 0000000..ce728ef
--- /dev/null
@@ -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
+    }
+}
@@ -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");
  *  ============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 (file)
index 0133148..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-    "cmHandles": [
-        "PNFDemo"
-    ]
-}
\ No newline at end of file
index 53b7d40..47bb43b 100644 (file)
@@ -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
index 8069bb7..d4615e7 100644 (file)
@@ -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
index dfad948..ea082b5 100644 (file)
@@ -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 (file)
index 0000000..3a64087
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+*** 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]>
index 203151b..1bc7f4f 100644 (file)
@@ -1,6 +1,6 @@
 .. This work is licensed under a Creative Commons Attribution 4.0 International License.
 .. http://creativecommons.org/licenses/by/4.0
-.. Copyright (C) 2021 Nordix Foundation
+.. Copyright (C) 2021-2022 Nordix Foundation
 
 .. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
 .. _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::
+
+    !"#$%&'()*+,./\:;<=>?@[]^`{|}~
index 2fc8d7f..983252f 100644 (file)
@@ -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
index 154a441..30896f6 100644 (file)
@@ -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
index b7a6563..a43190b 100644 (file)
@@ -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:
index bc46681..e8a75d9 100644 (file)
@@ -1,6 +1,6 @@
 .. This work is licensed under a Creative Commons Attribution 4.0 International License.
 .. http://creativecommons.org/licenses/by/4.0
-.. Copyright (C) 2021 Nordix Foundation
+.. Copyright (C) 2021-2022 Nordix Foundation
 
 .. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
 .. _design:
@@ -20,17 +20,137 @@ The CPS path parameter is used for querying xpaths. CPS path is inspired by the
 
 This section describes the functionality currently supported by CPS Path.
 
-Sample Data
-===========
+Sample Yang Model
+=================
 
-The xml below describes some basic data to be used to illustrate the CPS Path functionality.
+.. code-block::
+
+  module stores {
+      yang-version 1.1;
+      namespace "org:onap:ccsdk:sample";
+
+      prefix book-store;
+
+      revision "2020-09-15" {
+          description
+            "Sample Model";
+      }
+      container shops {
+
+          container bookstore {
+
+              leaf bookstore-name {
+                  type string;
+              }
+
+              leaf name {
+                  type string;
+              }
+
+              list categories {
+
+                  key "code";
+
+                  leaf code {
+                      type uint16;
+                  }
+
+                  leaf name {
+                      type string;
+                  }
+
+                  leaf numberOfBooks {
+                      type uint16;
+                  }
+
+                  container books {
+
+                      list book {
+                          key title;
+
+                          leaf title {
+                              type string;
+                          }
+                          leaf price {
+                              type uint16;
+                          }
+                          leaf-list label {
+                              type string;
+                          }
+                          leaf-list edition {
+                              type string;
+                          }
+                      }
+                  }
+              }
+          }
+      }
+  }
+
+**Note.** 'categories' is a Yang List and 'code' is its key leaf. All other data nodes are Yang Containers. 'label' and 'edition' are both leaf-lists.
+
+**Note.** CPS accepts only json data. The xml data presented here is for illustration purposes only.
+
+The json and xml below describes some basic data to be used to illustrate the CPS Path functionality.
+
+Sample Data in Json
+===================
+
+.. code-block:: json
+
+    {
+      "shops": {
+        "bookstore": {
+          "bookstore-name": "Chapters",
+          "name": "Chapters",
+          "categories": [
+            {
+              "code": 1,
+              "name": "SciFi",
+              "numberOfBooks": 2,
+              "books": {
+                "book": [
+                  {
+                    "title": "2001: A Space Odyssey",
+                    "price": 5,
+                    "label": ["sale", "classic"],
+                    "edition": ["1968", "2018"]
+                  },
+                  {
+                    "title": "Dune",
+                    "price": 5,
+                    "label": ["classic"],
+                    "edition": ["1965"]
+                  }
+                ]
+              }
+            },
+            {
+              "code": 2,
+              "name": "Kids",
+              "numberOfBooks": 1,
+              "books": {
+                "book": [
+                  {
+                    "title": "Matilda"
+                  }
+                ]
+              }
+            }
+          ]
+        }
+      }
+    }
+
+Sample Data in XML
+==================
 
 .. code-block:: xml
 
     <shops>
        <bookstore name="Chapters">
           <bookstore-name>Chapters</bookstore-name>
-          <categories code="1" name="SciFi" numberOfBooks="2">
+          <categories code=1 name="SciFi" numberOfBooks="2">
              <books>
                 <book title="2001: A Space Odyssey" price="5">
                    <label>sale</label>
@@ -44,7 +164,7 @@ The xml below describes some basic data to be used to illustrate the CPS Path fu
                 </book>
              </books>
           </categories>
-          <categories code="2" name="Kids" numberOfBooks="1">
+          <categories code=2 name="Kids" numberOfBooks="1">
              <books>
                 <book title="Matilda" />
              </books>
@@ -52,8 +172,6 @@ The xml below describes some basic data to be used to illustrate the CPS Path fu
        </bookstore>
     </shops>
 
-**Note.** 'categories' is a Yang List and 'code' is its key leaf. All other data nodes are Yang Containers. 'label' and 'edition' are both leaf-lists.
-
 General Notes
 =============
 
@@ -79,12 +197,14 @@ absolute-path
 
 **Examples**
   - ``/shops/bookstore``
-  - ``/shops/bookstore/categories[@code=1]``
-  - ``/shops/bookstore/categories[@code=1]/book``
+  - ``/shops/bookstore/categories[@code='1']/books``
+  - ``/shops/bookstore/categories[@code='1']/books/book[@title='2001: A Space Odyssey']``
 
 **Limitations**
   - Absolute paths must start with the top element (data node) as per the model tree.
   - Each list reference must include a valid instance reference to the key for that list. Except when it is the last element.
+  - The Absolute path to list with integer key will not work. It needs to be surrounded with a single quote ([@code='1'])
+    as if it is a string. This will be fixed in `CPS-961 <https://jira.onap.org/browse/CPS-961>`_
 
 descendant-path
 ---------------
@@ -95,7 +215,7 @@ descendant-path
 
 **Examples**
   - ``//bookstore``
-  - ``//categories[@code=1]/book``
+  - ``//categories[@code='1']/books``
   - ``//bookstore/categories``
 
 **Limitations**
@@ -113,7 +233,7 @@ leaf-conditions
   - ``/shops/bookstore/categories[@numberOfBooks=1]``
   - ``//categories[@name="Kids"]``
   - ``//categories[@name='Kids']``
-  - ``//categories[@code=1]/books/book[@title='Dune' and @price=5]``
+  - ``//categories[@code='1']/books/book[@title='Dune' and @price=5]``
 
 **Limitations**
   - Only the last list or container can be queried leaf values. Any ancestor list will have to be referenced by its key name-value pair(s).
@@ -156,9 +276,9 @@ The ancestor axis can be added to any CPS path query but has to be the last part
 
 **Examples**
   - ``//book/ancestor::categories``
-  - ``//categories[@genre="SciFi"]/book/ancestor::bookstore``
-  - ``book/ancestor::categories[@code=1]/books``
-  - ``//book/label[text()="classic"]/ancestor::shop``
+  - ``//categories[@code='2']/books/ancestor::bookstore``
+  - ``//book/ancestor::categories[@code='1']/books``
+  - ``//book/label[text()="classic"]/ancestor::shops``
 
 **Limitations**
   - Ancestor list elements can only be addressed using the list key leaf.
index 6f450c1..06e1ddc 100644 (file)
@@ -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
 ============================
index 62ba5e8..eaf3646 100755 (executable)
@@ -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>`
index 4b69dd8..cde6f6d 100644 (file)
@@ -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>`_
index 0ca0547..a584b58 100755 (executable)
@@ -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
index 5a3d2f1..3b3441a 100644 (file)
@@ -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
index d42d89a..d1181d3 100644 (file)
@@ -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 87398bd..23ef44b 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@
 \r
     <groupId>org.onap.cps</groupId>\r
     <artifactId>cps-aggregator</artifactId>\r
-    <version>3.0.0-SNAPSHOT</version>\r
+    <version>3.1.0-SNAPSHOT</version>\r
     <packaging>pom</packaging>\r
 \r
     <name>cps</name>\r
diff --git a/releases/3.0.0-container.yaml b/releases/3.0.0-container.yaml
new file mode 100644 (file)
index 0000000..f227bdb
--- /dev/null
@@ -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 (file)
index 0000000..60dd811
--- /dev/null
@@ -0,0 +1,4 @@
+distribution_type: maven
+log_dir: cps-maven-stage-master/504/
+project: cps
+version: 3.0.0
index 50cef48..df033a3 100644 (file)
@@ -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>
         <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>
index 17f2daa..870b994 100755 (executable)
@@ -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}