summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--checkstyle/src/main/resources/cps-java-style.xml1
-rwxr-xr-xcps-parent/pom.xml51
-rwxr-xr-xcps-rest/pom.xml41
-rw-r--r--cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java88
-rw-r--r--cps-rest/src/main/resources/application.yml5
-rw-r--r--cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy69
-rw-r--r--cps-rest/src/test/groovy/org/onap/cps/rest/utils/MultipartFileUtilSpec.groovy67
-rw-r--r--cps-rest/src/test/resources/no-yang-files.zipbin0 -> 390 bytes
-rw-r--r--cps-rest/src/test/resources/yang-files-multiple-sets.zipbin0 -> 1304 bytes
-rw-r--r--cps-rest/src/test/resources/yang-files-set.zipbin0 -> 655 bytes
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java39
-rwxr-xr-xcps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java3
-rwxr-xr-xcps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java13
-rwxr-xr-xcps-ri/src/main/resources/schema.sql14
-rw-r--r--cps-ri/src/test/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceTest.java140
-rwxr-xr-xcps-ri/src/test/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceTest.java2
-rw-r--r--cps-ri/src/test/resources/data/fragment.sql9
-rw-r--r--cps-service/pom.xml18
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java31
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java70
-rw-r--r--cps-service/src/main/java/org/onap/cps/config/CacheConfig.java29
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java11
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy39
-rwxr-xr-xcps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy3
24 files changed, 552 insertions, 191 deletions
diff --git a/checkstyle/src/main/resources/cps-java-style.xml b/checkstyle/src/main/resources/cps-java-style.xml
index ebb9a19e6..67b286369 100644
--- a/checkstyle/src/main/resources/cps-java-style.xml
+++ b/checkstyle/src/main/resources/cps-java-style.xml
@@ -30,5 +30,6 @@
<property name="tokens" value="VARIABLE_DEF,PARAMETER_DEF"/>
<property name="validateEnhancedForLoopVariable" value="true"/>
</module>
+ <module name="UnusedImports"/>
</module>
</module> \ No newline at end of file
diff --git a/cps-parent/pom.xml b/cps-parent/pom.xml
index f427555aa..c3b61541f 100755
--- a/cps-parent/pom.xml
+++ b/cps-parent/pom.xml
@@ -59,57 +59,6 @@
</snapshotRepository>
</distributionManagement>
- <profiles>
- <profile>
- <id>docker</id>
- <activation>
- <activeByDefault>false</activeByDefault>
- </activation>
- <build>
- <pluginManagement>
- <plugins>
- <plugin>
- <groupId>com.google.cloud.tools</groupId>
- <artifactId>jib-maven-plugin</artifactId>
- <version>${jib-maven-plugin.version}</version>
- <configuration>
- <container>
- <mainClass>${app}</mainClass>
- <creationTime>USE_CURRENT_TIMESTAMP</creationTime>
- </container>
- <from>
- <image>${base.image}</image>
- </from>
- <to>
- <image>${repository.name}</image>
- <tags>
- <tag>${tag.version}</tag>
- </tags>
- </to>
- </configuration>
- <executions>
- <execution>
- <phase>package</phase>
- <id>build</id>
- <goals>
- <goal>dockerBuild</goal>
- </goals>
- </execution>
- <execution>
- <phase>deploy</phase>
- <id>buildAndPush</id>
- <goals>
- <goal>build</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </pluginManagement>
- </build>
- </profile>
- </profiles>
-
<dependencyManagement>
<dependencies>
<dependency>
diff --git a/cps-rest/pom.xml b/cps-rest/pom.xml
index 128c56680..49c3267dd 100755
--- a/cps-rest/pom.xml
+++ b/cps-rest/pom.xml
@@ -1,6 +1,6 @@
<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 https://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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.onap.cps</groupId>
@@ -98,10 +98,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
- <plugin>
- <groupId>com.google.cloud.tools</groupId>
- <artifactId>jib-maven-plugin</artifactId>
- </plugin>
<!-- Swagger code generation. -->
<plugin>
<groupId>io.swagger.codegen.v3</groupId>
@@ -109,4 +105,37 @@
</plugin>
</plugins>
</build>
+
+ <profiles>
+ <profile>
+ <id>docker</id>
+ <activation>
+ <activeByDefault>false</activeByDefault>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.google.cloud.tools</groupId>
+ <artifactId>jib-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <id>build</id>
+ <goals>
+ <goal>dockerBuild</goal>
+ </goals>
+ </execution>
+ <execution>
+ <phase>deploy</phase>
+ <id>buildAndPush</id>
+ <goals>
+ <goal>build</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
</project>
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java b/cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java
index c53d1a42a..532a0ca84 100644
--- a/cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java
+++ b/cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java
@@ -19,13 +19,18 @@
package org.onap.cps.rest.utils;
-import static com.google.common.base.Preconditions.checkNotNull;
import static org.opendaylight.yangtools.yang.common.YangConstants.RFC6020_YANG_FILE_EXTENSION;
import com.google.common.collect.ImmutableMap;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Locale;
import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.onap.cps.spi.exceptions.CpsException;
@@ -35,26 +40,81 @@ import org.springframework.web.multipart.MultipartFile;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MultipartFileUtil {
+ private static final String ZIP_FILE_EXTENSION = ".zip";
+ private static final String YANG_FILE_EXTENSION = RFC6020_YANG_FILE_EXTENSION;
+ private static final int READ_BUFFER_SIZE = 1024;
+
/**
* Extracts yang resources from multipart file instance.
*
* @param multipartFile the yang file uploaded
* @return yang resources as {map} where the key is original file name, and the value is file content
- * @throws ModelValidationException if the file name extension is not '.yang'
+ * @throws ModelValidationException if the file name extension is not '.yang' or '.zip'
+ * or if zip archive contain no yang files
* @throws CpsException if the file content cannot be read
*/
public static Map<String, String> extractYangResourcesMap(final MultipartFile multipartFile) {
- return ImmutableMap.of(extractYangResourceName(multipartFile), extractYangResourceContent(multipartFile));
+ final String originalFileName = multipartFile.getOriginalFilename();
+ if (resourceNameEndsWithExtension(originalFileName, YANG_FILE_EXTENSION)) {
+ return ImmutableMap.of(originalFileName, extractYangResourceContent(multipartFile));
+ }
+ if (resourceNameEndsWithExtension(originalFileName, ZIP_FILE_EXTENSION)) {
+ return extractYangResourcesMapFromZipArchive(multipartFile);
+ }
+ throw new ModelValidationException("Unsupported file type.",
+ String.format("Filename %s matches none of expected extensions: %s", originalFileName,
+ Arrays.asList(YANG_FILE_EXTENSION, ZIP_FILE_EXTENSION)));
+ }
+
+ private static Map<String, String> extractYangResourcesMapFromZipArchive(final MultipartFile multipartFile) {
+ final ImmutableMap.Builder<String, String> yangResourceMapBuilder = ImmutableMap.builder();
+
+ try (
+ final InputStream inputStream = multipartFile.getInputStream();
+ final ZipInputStream zipInputStream = new ZipInputStream(inputStream);
+ ) {
+ ZipEntry zipEntry;
+ while ((zipEntry = zipInputStream.getNextEntry()) != null) {
+ extractZipEntryToMapIfApplicable(yangResourceMapBuilder, zipEntry, zipInputStream);
+ }
+ zipInputStream.closeEntry();
+
+ } catch (final IOException e) {
+ throw new CpsException("Cannot extract resources from zip archive.", e.getMessage(), e);
+ }
+
+ try {
+ final Map<String, String> yangResourceMap = yangResourceMapBuilder.build();
+ if (yangResourceMap.isEmpty()) {
+ throw new ModelValidationException("Archive contains no YANG resources.",
+ String.format("Archive contains no files having %s extension.", YANG_FILE_EXTENSION));
+ }
+ return yangResourceMap;
+
+ } catch (final IllegalArgumentException e) {
+ throw new ModelValidationException("Invalid ZIP archive content.",
+ "Multiple resources with same name detected.", e);
+ }
}
- private static String extractYangResourceName(final MultipartFile multipartFile) {
- final String fileName = checkNotNull(multipartFile.getOriginalFilename(), "Missing filename.");
- if (!fileName.endsWith(RFC6020_YANG_FILE_EXTENSION)) {
- throw new ModelValidationException("Unsupported file type.",
- String.format("Filename %s does not end with '%s'", fileName, RFC6020_YANG_FILE_EXTENSION));
+ private static void extractZipEntryToMapIfApplicable(
+ final ImmutableMap.Builder<String, String> yangResourceMapBuilder, final ZipEntry zipEntry,
+ final ZipInputStream zipInputStream) throws IOException {
+
+ final String yangResourceName = extractResourceNameFromPath(zipEntry.getName());
+ if (zipEntry.isDirectory() || !resourceNameEndsWithExtension(yangResourceName, YANG_FILE_EXTENSION)) {
+ return;
}
- return fileName;
+ yangResourceMapBuilder.put(yangResourceName, extractYangResourceContent(zipInputStream));
+ }
+
+ private static boolean resourceNameEndsWithExtension(final String resourceName, final String extension) {
+ return resourceName != null && resourceName.toLowerCase(Locale.ENGLISH).endsWith(extension);
+ }
+
+ private static String extractResourceNameFromPath(final String path) {
+ return path == null ? "" : path.replaceAll("^.*[\\\\/]", "");
}
private static String extractYangResourceContent(final MultipartFile multipartFile) {
@@ -65,4 +125,14 @@ public class MultipartFileUtil {
}
}
+ private static String extractYangResourceContent(final ZipInputStream zipInputStream) throws IOException {
+ try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
+ final byte[] buffer = new byte[READ_BUFFER_SIZE];
+ int numberOfBytesRead;
+ while ((numberOfBytesRead = zipInputStream.read(buffer, 0, READ_BUFFER_SIZE)) > 0) {
+ byteArrayOutputStream.write(buffer, 0, numberOfBytesRead);
+ }
+ return byteArrayOutputStream.toString(StandardCharsets.UTF_8);
+ }
+ }
}
diff --git a/cps-rest/src/main/resources/application.yml b/cps-rest/src/main/resources/application.yml
index b50a92662..0f2ef20df 100644
--- a/cps-rest/src/main/resources/application.yml
+++ b/cps-rest/src/main/resources/application.yml
@@ -24,6 +24,11 @@ spring:
driverClassName: org.postgresql.Driver
initialization-mode: always
+ cache:
+ type: caffeine
+ cache-names: yangSchema
+ caffeine:
+ spec: maximumSize=10000,expireAfterAccess=10m
# Actuator
management:
endpoints:
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
index 2d4fe9bb0..540d6224a 100644
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
@@ -38,6 +38,7 @@ import org.springframework.test.web.servlet.MockMvc
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import spock.lang.Specification
+import spock.lang.Unroll
import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
@@ -89,29 +90,66 @@ class AdminRestControllerSpec extends Specification {
response.status == HttpStatus.BAD_REQUEST.value()
}
- def 'Create schema set from yang file'() {
+ def 'Create schema set from yang file.'() {
def yangResourceMapCapture
- given:
+ given: 'single yang file'
def multipartFile = createMultipartFile("filename.yang", "content")
- when:
+ when: 'file uploaded with schema set create request'
def response = performCreateSchemaSetRequest(multipartFile)
- then: 'Service method is invoked with expected parameters'
+ then: 'associated service method is invoked with expected parameters'
1 * mockCpsModuleService.createSchemaSet('test-dataspace', 'test-schema-set', _) >>
{ args -> yangResourceMapCapture = args[2] }
yangResourceMapCapture['filename.yang'] == 'content'
- and: 'Response code indicates success'
+ and: 'response code indicates success'
response.status == HttpStatus.CREATED.value()
}
- def 'Create schema set from file with invalid filename extension'() {
- given:
+ def 'Create schema set from zip archive.'() {
+ def yangResourceMapCapture
+ given: 'zip archive with multiple .yang files inside'
+ def multipartFile = createZipMultipartFileFromResource("/yang-files-set.zip")
+ when: 'file uploaded with schema set create request'
+ def response = performCreateSchemaSetRequest(multipartFile)
+ then: 'associated service method is invoked with expected parameters'
+ 1 * mockCpsModuleService.createSchemaSet('test-dataspace', 'test-schema-set', _) >>
+ { args -> yangResourceMapCapture = args[2] }
+ yangResourceMapCapture['assembly.yang'] == "fake assembly content 1\n"
+ yangResourceMapCapture['component.yang'] == "fake component content 1\n"
+ and: 'response code indicates success'
+ response.status == HttpStatus.CREATED.value()
+ }
+
+ @Unroll
+ def 'Create schema set from zip archive having #caseDescriptor.'() {
+ when: 'zip archive having #caseDescriptor is uploaded with create schema set request'
+ def response = performCreateSchemaSetRequest(multipartFile)
+ then: 'create schema set rejected'
+ response.status == HttpStatus.BAD_REQUEST.value()
+ where: 'following cases are tested'
+ caseDescriptor | multipartFile
+ 'no .yang files inside' | createZipMultipartFileFromResource("/no-yang-files.zip")
+ 'multiple .yang files with same name' | createZipMultipartFileFromResource("/yang-files-multiple-sets.zip")
+ }
+
+ def 'Create schema set from file with unsupported filename extension.'() {
+ given: 'file with unsupported filename extension (.doc)'
def multipartFile = createMultipartFile("filename.doc", "content")
- when:
+ when: 'file uploaded with schema set create request'
def response = performCreateSchemaSetRequest(multipartFile)
- then: 'Create schema fails'
+ then: 'create schema set rejected'
response.status == HttpStatus.BAD_REQUEST.value()
}
+ @Unroll
+ def 'Create schema set from #fileType file with IOException occurrence on processing.'() {
+ when: 'file uploaded with schema set create request'
+ def response = performCreateSchemaSetRequest(createMultipartFileForIOException(fileType))
+ then: 'the error response returned indicating internal server error occurrence'
+ response.status == HttpStatus.INTERNAL_SERVER_ERROR.value()
+ where: 'following file types are used'
+ fileType << ['YANG', 'ZIP']
+ }
+
def 'Delete schema set.'() {
when: 'delete schema set endpoint is invoked'
def response = performDeleteRequest(schemaSetEndpoint)
@@ -142,6 +180,19 @@ class AdminRestControllerSpec extends Specification {
return new MockMultipartFile("file", filename, "text/plain", content.getBytes())
}
+ def createZipMultipartFileFromResource(resourcePath) {
+ return new MockMultipartFile("file", "test.zip", "application/zip",
+ getClass().getResource(resourcePath).getBytes())
+ }
+
+ def createMultipartFileForIOException(extension) {
+ def multipartFile = Mock(MockMultipartFile)
+ multipartFile.getOriginalFilename() >> "TEST." + extension
+ multipartFile.getBytes() >> { throw new IOException() }
+ multipartFile.getInputStream() >> { throw new IOException() }
+ return multipartFile
+ }
+
def performCreateSchemaSetRequest(multipartFile) {
return mvc.perform(
multipart("$basePath$schemaSetsEndpoint")
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/utils/MultipartFileUtilSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/utils/MultipartFileUtilSpec.groovy
index ba5aa4cac..3e2bdec37 100644
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/utils/MultipartFileUtilSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/utils/MultipartFileUtilSpec.groovy
@@ -19,30 +19,79 @@
package org.onap.cps.rest.utils
+import org.onap.cps.spi.exceptions.CpsException
import org.onap.cps.spi.exceptions.ModelValidationException
import org.springframework.mock.web.MockMultipartFile
+import org.springframework.web.multipart.MultipartFile
import spock.lang.Specification
+import spock.lang.Unroll
class MultipartFileUtilSpec extends Specification {
- def 'Extract yang resource from multipart file'() {
- given:
+ def 'Extract yang resource from yang file.'() {
+ given: 'uploaded yang file'
def multipartFile = new MockMultipartFile("file", "filename.yang", "text/plain", "content".getBytes())
- when:
+ when: 'resources are extracted from the file'
def result = MultipartFileUtil.extractYangResourcesMap(multipartFile)
- then:
- assert result != null
+ then: 'the expected name and content are extracted as result'
assert result.size() == 1
assert result.get("filename.yang") == "content"
}
- def 'Extract yang resource from file with invalid filename extension'() {
- given:
+ def 'Extract yang resources from zip archive.'() {
+ given: 'uploaded zip archive containing 2 yang files and 1 not yang (json) file'
+ def multipartFile = new MockMultipartFile("file", "TEST.ZIP", "application/zip",
+ getClass().getResource("/yang-files-set.zip").getBytes())
+ when: 'resources are extracted from zip file'
+ def result = MultipartFileUtil.extractYangResourcesMap(multipartFile)
+ then: 'information from yang files is extracted, not yang file (json) is ignored'
+ assert result.size() == 2
+ assert result["assembly.yang"] == "fake assembly content 1\n"
+ assert result["component.yang"] == "fake component content 1\n"
+ }
+
+ @Unroll
+ def 'Extract resources from zip archive having #caseDescriptor.'() {
+ when: 'attempt to extract resources from zip file is performed'
+ MultipartFileUtil.extractYangResourcesMap(multipartFile)
+ then: 'the validation exception is thrown indicating invalid zip file content'
+ thrown(ModelValidationException)
+ where: 'following cases are tested'
+ caseDescriptor | multipartFile
+ 'text files only' | multipartZipFileFromResource("/no-yang-files.zip")
+ 'multiple yang file with same name' | multipartZipFileFromResource("/yang-files-multiple-sets.zip")
+ }
+
+ def 'Extract yang resource from a file with invalid filename extension.'() {
+ given: 'uploaded file with unsupported (.doc) exception'
def multipartFile = new MockMultipartFile("file", "filename.doc", "text/plain", "content".getBytes())
- when:
+ when: 'attempt to extract resources from the file is performed'
MultipartFileUtil.extractYangResourcesMap(multipartFile)
- then:
+ then: 'validation exception is thrown indicating the file type is not supported'
thrown(ModelValidationException)
}
+ @Unroll
+ def 'IOException thrown during yang resources extraction from #fileType file.'() {
+ when: 'attempt to extract resources from the file is performed'
+ MultipartFileUtil.extractYangResourcesMap(multipartFileForIOException(fileType))
+ then: 'CpsException is thrown indicating the internal error occurrence'
+ thrown(CpsException)
+ where: 'following file types are used'
+ fileType << ['YANG', 'ZIP']
+ }
+
+ def multipartZipFileFromResource(resourcePath) {
+ return new MockMultipartFile("file", "TEST.ZIP", "application/zip",
+ getClass().getResource(resourcePath).getBytes())
+ }
+
+ def multipartFileForIOException(extension) {
+ def multipartFile = Mock(MultipartFile)
+ multipartFile.getOriginalFilename() >> "TEST." + extension
+ multipartFile.getBytes() >> { throw new IOException() }
+ multipartFile.getInputStream() >> { throw new IOException() }
+ return multipartFile
+ }
+
}
diff --git a/cps-rest/src/test/resources/no-yang-files.zip b/cps-rest/src/test/resources/no-yang-files.zip
new file mode 100644
index 000000000..83f6963b1
--- /dev/null
+++ b/cps-rest/src/test/resources/no-yang-files.zip
Binary files differ
diff --git a/cps-rest/src/test/resources/yang-files-multiple-sets.zip b/cps-rest/src/test/resources/yang-files-multiple-sets.zip
new file mode 100644
index 000000000..855e87bb2
--- /dev/null
+++ b/cps-rest/src/test/resources/yang-files-multiple-sets.zip
Binary files differ
diff --git a/cps-rest/src/test/resources/yang-files-set.zip b/cps-rest/src/test/resources/yang-files-set.zip
new file mode 100644
index 000000000..09236ce47
--- /dev/null
+++ b/cps-rest/src/test/resources/yang-files-set.zip
Binary files differ
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
index 283d64682..c73b65ddd 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
@@ -46,7 +46,19 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
@Autowired
private FragmentRepository fragmentRepository;
- private static Gson GSON = new GsonBuilder().create();
+ private static final Gson GSON = new GsonBuilder().create();
+
+ @Override
+ public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentXpath,
+ final DataNode dataNode) {
+ final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+ final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+ final FragmentEntity parentFragment =
+ fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, parentXpath);
+ final FragmentEntity childFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNode);
+ parentFragment.getChildFragments().add(childFragment);
+ fragmentRepository.save(parentFragment);
+ }
@Override
public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) {
@@ -68,21 +80,26 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
*/
private static FragmentEntity convertToFragmentWithAllDescendants(final DataspaceEntity dataspaceEntity,
final AnchorEntity anchorEntity, final DataNode dataNodeToBeConverted) {
- final FragmentEntity parentFragment = FragmentEntity.builder()
- .dataspace(dataspaceEntity)
- .anchor(anchorEntity)
- .xpath(dataNodeToBeConverted.getXpath())
- .attributes(GSON.toJson(dataNodeToBeConverted.getLeaves()))
- .build();
-
- final Builder<FragmentEntity> fragmentEntityBuilder = ImmutableSet.builder();
+ final FragmentEntity parentFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNodeToBeConverted);
+ final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
final FragmentEntity childFragment =
convertToFragmentWithAllDescendants(parentFragment.getDataspace(), parentFragment.getAnchor(),
childDataNode);
- fragmentEntityBuilder.add(childFragment);
+ childFragmentsImmutableSetBuilder.add(childFragment);
}
- parentFragment.setChildFragments(fragmentEntityBuilder.build());
+ parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build());
return parentFragment;
}
+
+ private static FragmentEntity toFragmentEntity(final DataspaceEntity dataspaceEntity,
+ final AnchorEntity anchorEntity,
+ final DataNode dataNode) {
+ return FragmentEntity.builder()
+ .dataspace(dataspaceEntity)
+ .anchor(anchorEntity)
+ .xpath(dataNode.getXpath())
+ .attributes(GSON.toJson(dataNode.getLeaves()))
+ .build();
+ }
}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java
index cac41ca9d..b28beb42c 100755
--- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java
@@ -127,8 +127,7 @@ public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceServ
}
@Override
- public Map<String, String> getYangSchemaSetResources(final String dataspaceName,
- final String anchorName) {
+ public Map<String, String> getYangSchemaSetResources(final String dataspaceName, final String anchorName) {
final Anchor anchor = cpsAdminPersistenceService.getAnchor(dataspaceName, anchorName);
return getYangSchemaResources(dataspaceName, anchor.getSchemaSetName());
}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
index 4d4494396..6fc956c44 100755
--- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
@@ -22,9 +22,13 @@
package org.onap.cps.spi.repository;
import java.util.Collection;
+import java.util.Optional;
import javax.validation.constraints.NotNull;
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.onap.cps.spi.entities.AnchorEntity;
+import org.onap.cps.spi.entities.DataspaceEntity;
import org.onap.cps.spi.entities.FragmentEntity;
+import org.onap.cps.spi.exceptions.NotFoundInDataspaceException;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
@@ -34,6 +38,15 @@ import org.springframework.stereotype.Repository;
@Repository
public interface FragmentRepository extends JpaRepository<FragmentEntity, Long> {
+ Optional<FragmentEntity> findByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity,
+ @NonNull AnchorEntity anchorEntity, @NonNull String xpath);
+
+ default FragmentEntity getByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity,
+ @NonNull AnchorEntity anchorEntity, @NonNull String xpath) {
+ return findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, xpath)
+ .orElseThrow(() -> new NotFoundInDataspaceException(dataspaceEntity.getName(), xpath));
+ }
+
@Modifying
@Query("DELETE FROM FragmentEntity fe WHERE fe.anchor IN (:anchors)")
void deleteByAnchorIn(@NotNull @Param("anchors") Collection<AnchorEntity> anchorEntities);
diff --git a/cps-ri/src/main/resources/schema.sql b/cps-ri/src/main/resources/schema.sql
index d37d932aa..2735ca489 100755
--- a/cps-ri/src/main/resources/schema.sql
+++ b/cps-ri/src/main/resources/schema.sql
@@ -42,17 +42,6 @@ CREATE TABLE IF NOT EXISTS SCHEMA_SET_YANG_RESOURCES
CONSTRAINT SCHEMA_SET_RESOURCE FOREIGN KEY (SCHEMA_SET_ID) REFERENCES SCHEMA_SET(ID) ON DELETE CASCADE
);
-CREATE TABLE IF NOT EXISTS MODULE
-(
- ID SERIAL PRIMARY KEY,
- NAMESPACE TEXT NOT NULL,
- REVISION TEXT NOT NULL,
- MODULE_CONTENT TEXT NOT NULL,
- DATASPACE_ID BIGINT NOT NULL,
- UNIQUE (DATASPACE_ID, NAMESPACE, REVISION),
- CONSTRAINT MODULE_DATASPACE FOREIGN KEY (DATASPACE_ID) REFERENCES DATASPACE (id) ON UPDATE CASCADE ON DELETE CASCADE
-);
-
CREATE TABLE IF NOT EXISTS ANCHOR
(
ID BIGSERIAL PRIMARY KEY,
@@ -93,5 +82,4 @@ CREATE INDEX IF NOT EXISTS "FKI_SCHEMA_NODE_ID_TO_ID" ON FRAGMENT USING
CREATE INDEX IF NOT EXISTS "FKI_RELATION_TYPE_ID_FK" ON RELATION USING BTREE(RELATION_TYPE_ID);
CREATE INDEX IF NOT EXISTS "FKI_RELATIONS_FROM_ID_FK" ON RELATION USING BTREE(FROM_FRAGMENT_ID);
CREATE INDEX IF NOT EXISTS "FKI_RELATIONS_TO_ID_FK" ON RELATION USING BTREE(TO_FRAGMENT_ID);
-CREATE INDEX IF NOT EXISTS "PERF_MODULE_MODULE_CONTENT" ON MODULE USING BTREE(MODULE_CONTENT);
-CREATE UNIQUE INDEX IF NOT EXISTS "UQ_FRAGMENT_XPATH"ON FRAGMENT USING btree(xpath COLLATE pg_catalog."default" text_pattern_ops, dataspace_id);
+CREATE UNIQUE INDEX IF NOT EXISTS "UQ_FRAGMENT_XPATH"ON FRAGMENT USING btree(xpath COLLATE pg_catalog."default" text_pattern_ops, dataspace_id); \ No newline at end of file
diff --git a/cps-ri/src/test/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceTest.java b/cps-ri/src/test/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceTest.java
index db2941c19..de0942c3c 100644
--- a/cps-ri/src/test/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceTest.java
+++ b/cps-ri/src/test/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceTest.java
@@ -22,7 +22,9 @@ package org.onap.cps.spi.impl;
import static junit.framework.TestCase.assertEquals;
import com.google.common.collect.ImmutableSet;
+import java.util.Arrays;
import java.util.Collections;
+import org.assertj.core.api.Assertions;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -31,6 +33,7 @@ import org.onap.cps.spi.CpsDataPersistenceService;
import org.onap.cps.spi.entities.FragmentEntity;
import org.onap.cps.spi.exceptions.AnchorNotFoundException;
import org.onap.cps.spi.exceptions.DataspaceNotFoundException;
+import org.onap.cps.spi.exceptions.NotFoundInDataspaceException;
import org.onap.cps.spi.model.DataNode;
import org.onap.cps.spi.repository.FragmentRepository;
import org.springframework.beans.factory.annotation.Autowired;
@@ -47,22 +50,23 @@ public class CpsDataPersistenceServiceTest {
private static final String CLEAR_DATA = "/data/clear-all.sql";
private static final String SET_DATA = "/data/fragment.sql";
- private static final String NON_EXISTING_DATASPACE_NAME = "NON EXISTING DATASPACE";
private static final String DATASPACE_NAME = "DATASPACE-001";
private static final String ANCHOR_NAME1 = "ANCHOR-001";
- private static final String NON_EXISTING_ANCHOR_NAME = "NON EXISTING ANCHOR";
- private static final String PARENT_XPATH = "/parent";
- private static final String CHILD_XPATH = "/parent/child";
- private static final String GRAND_CHILD_XPATH = "/parent/child/grandchild";
- private static final String PARENT_XPATH_NEW = "/parent-new";
- private static final String CHILD_XPATH_NEW = "/parent/child-new";
- private static final String GRAND_CHILD_XPATH_NEW = "/parent/child/grandchild-new";
- private static final long PARENT_ID = 3001;
- private static final long CHILD_ID = 3002;
- private static final long GRAND_CHILD_ID = 3003;
- private static final long PARENT_ID_NEW = 2;
- private static final long CHILD_ID_NEW = 3;
- private static final long GRAND_CHILD_ID_NEW = 4;
+
+ private static final long PARENT_ID_4001 = 4001;
+ private static final long PARENT_ID_4002 = 4002;
+ private static final long PARENT_ID_4003 = 4003;
+ private static final String PARENT_XPATH1 = "/parent-1";
+ private static final String PARENT_XPATH2 = "/parent-2";
+ private static final String PARENT_XPATH3 = "/parent-3";
+
+ private static final long CHILD_ID_4004 = 4004;
+ private static final String CHILD_XPATH1 = "/parent-1/child-1";
+ private static final String CHILD_XPATH2 = "/parent-2/child-2";
+
+ private static final long GRAND_CHILD_ID_4006 = 4006;
+ private static final String GRAND_CHILD_XPATH1 = "/parent-1/child-1/grandchild-1";
+
@ClassRule
public static DatabaseTestContainer databaseTestContainer = DatabaseTestContainer.getInstance();
@@ -76,48 +80,90 @@ public class CpsDataPersistenceServiceTest {
@Test
@Sql({CLEAR_DATA, SET_DATA})
public void testGetFragmentsWithChildAndGrandChild() {
- final FragmentEntity parentFragment = fragmentRepository.findById(PARENT_ID).orElseThrow();
- final FragmentEntity childFragment = fragmentRepository.findById(CHILD_ID).orElseThrow();
- final FragmentEntity grandChildFragment = fragmentRepository.findById(GRAND_CHILD_ID).orElseThrow();
-
- assertFragment(parentFragment, childFragment, grandChildFragment, PARENT_XPATH, CHILD_XPATH, GRAND_CHILD_XPATH);
+ final FragmentEntity parentFragment = fragmentRepository.findById(PARENT_ID_4001).orElseThrow();
+ final FragmentEntity childFragment = fragmentRepository.findById(CHILD_ID_4004).orElseThrow();
+ final FragmentEntity grandChildFragment = fragmentRepository.findById(GRAND_CHILD_ID_4006).orElseThrow();
+ assertFragment(parentFragment, childFragment, grandChildFragment, PARENT_XPATH1, CHILD_XPATH1,
+ GRAND_CHILD_XPATH1);
}
@Test(expected = DataspaceNotFoundException.class)
@Sql({CLEAR_DATA, SET_DATA})
public void testStoreDataNodeAtNonExistingDataspace() {
cpsDataPersistenceService
- .storeDataNode(NON_EXISTING_DATASPACE_NAME, ANCHOR_NAME1,
- createDataNodeWithChildAndGrandChild(PARENT_XPATH_NEW, CHILD_XPATH_NEW, GRAND_CHILD_XPATH_NEW));
+ .storeDataNode("Non Existing Dataspace Name", ANCHOR_NAME1, new DataNode());
}
@Test(expected = AnchorNotFoundException.class)
@Sql({CLEAR_DATA, SET_DATA})
public void testStoreDataNodeAtNonExistingAnchor() {
cpsDataPersistenceService
- .storeDataNode(DATASPACE_NAME, NON_EXISTING_ANCHOR_NAME,
- createDataNodeWithChildAndGrandChild(PARENT_XPATH_NEW, CHILD_XPATH_NEW, GRAND_CHILD_XPATH_NEW));
+ .storeDataNode(DATASPACE_NAME, "Non Existing Anchor Name", new DataNode());
}
@Test(expected = DataIntegrityViolationException.class)
@Sql({CLEAR_DATA, SET_DATA})
public void testStoreDataNodeWithIntegrityException() {
cpsDataPersistenceService.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1,
- createDataNodeWithChildAndGrandChild(PARENT_XPATH, CHILD_XPATH, GRAND_CHILD_XPATH));
+ createDataNodeTree(PARENT_XPATH1));
}
@Test
@Sql({CLEAR_DATA, SET_DATA})
- public void testStoreDataNodeWithChildrenAndGrandChildren() {
+ public void testStoreDataNodeWithChildAndGrandChild() {
+ final String parentXpath = "/parent-new";
+ final String childXpath = "/parent-new/child-new";
+ final String grandChildXpath = "/parent-new/child-new/grandchild-new";
+
cpsDataPersistenceService.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1,
- createDataNodeWithChildAndGrandChild(PARENT_XPATH_NEW, CHILD_XPATH_NEW, GRAND_CHILD_XPATH_NEW));
+ createDataNodeTree(parentXpath, childXpath, grandChildXpath));
+ final FragmentEntity parentFragment = getFragmentByXpath(parentXpath);
+ final FragmentEntity childFragment = getFragmentByXpath(childXpath);
+ final FragmentEntity grandChildFragment = getFragmentByXpath(grandChildXpath);
+ assertFragment(parentFragment, childFragment, grandChildFragment, parentXpath, childXpath,
+ grandChildXpath);
+ }
+
+ @Test
+ @Sql({CLEAR_DATA, SET_DATA})
+ public void testAddChildToFragmentThatHasOneChild() {
+ final String childXpath = "some-xpath";
+ final DataNode childDataNode = createDataNodeTree(childXpath);
+ cpsDataPersistenceService
+ .addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, PARENT_XPATH2, childDataNode);
+ final FragmentEntity parentFragment = fragmentRepository.findById(PARENT_ID_4002).orElseThrow();
+ Assertions.assertThat(parentFragment.getChildFragments())
+ .hasSize(2)
+ .extracting(FragmentEntity::getXpath)
+ .containsExactlyInAnyOrder(childXpath, CHILD_XPATH2);
+ }
+
+ @Test
+ @Sql({CLEAR_DATA, SET_DATA})
+ public void testAddChildToFragmentThatHasNoChild() {
+ final String childXpath = "some-xpath";
+ final DataNode childDataNode = createDataNodeTree(childXpath);
+ cpsDataPersistenceService
+ .addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, PARENT_XPATH3, childDataNode);
+ final FragmentEntity parentFragment = fragmentRepository.findById(PARENT_ID_4003).orElseThrow();
+ Assertions.assertThat(parentFragment.getChildFragments())
+ .hasSize(1)
+ .extracting(FragmentEntity::getXpath)
+ .containsExactlyInAnyOrder(childXpath);
+ }
- final FragmentEntity parentFragment = fragmentRepository.findById(PARENT_ID_NEW).orElseThrow();
- final FragmentEntity childFragment = fragmentRepository.findById(CHILD_ID_NEW).orElseThrow();
- final FragmentEntity grandChildFragment = fragmentRepository.findById(GRAND_CHILD_ID_NEW).orElseThrow();
+ @Test(expected = DataIntegrityViolationException.class)
+ @Sql({CLEAR_DATA, SET_DATA})
+ public void testAddAChildWithTheSameXpathAsExistingChild() {
+ cpsDataPersistenceService
+ .addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, PARENT_XPATH1, createDataNodeTree(CHILD_XPATH1));
+ }
- assertFragment(parentFragment, childFragment, grandChildFragment, PARENT_XPATH_NEW, CHILD_XPATH_NEW,
- GRAND_CHILD_XPATH_NEW);
+ @Test(expected = NotFoundInDataspaceException.class)
+ @Sql({CLEAR_DATA, SET_DATA})
+ public void testAddAChildWithToAParentThatDoesNotExist() {
+ cpsDataPersistenceService
+ .addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, "non-existing-xpath", createDataNodeTree("some-xpath"));
}
private void assertFragment(final FragmentEntity parentFragment, final FragmentEntity childFragment,
@@ -136,24 +182,18 @@ public class CpsDataPersistenceServiceTest {
assertEquals(ANCHOR_NAME1, grandChildFragment.getAnchor().getName());
}
- private DataNode createDataNodeWithChildAndGrandChild(final String parentXpath, final String childXpath,
- final String grandChildXpath) {
- final DataNode parentDataNode = DataNode.builder()
- .xpath(parentXpath)
- .build();
-
- final DataNode childDataNode = DataNode.builder()
- .xpath(childXpath)
- .childDataNodes(Collections.emptySet())
- .build();
-
- final DataNode grandChildDataNode = DataNode.builder()
- .xpath(grandChildXpath)
- .childDataNodes(Collections.emptySet())
- .build();
-
- parentDataNode.setChildDataNodes(ImmutableSet.of(childDataNode));
- childDataNode.setChildDataNodes(ImmutableSet.of(grandChildDataNode));
- return parentDataNode;
+ private FragmentEntity getFragmentByXpath(final String xpath) {
+ return fragmentRepository.findAll().stream()
+ .filter(fragment -> fragment.getXpath().contains(xpath)).findAny().orElseThrow();
+ }
+
+ private static DataNode createDataNodeTree(final String... xpaths) {
+ final DataNode dataNode = DataNode.builder().xpath(xpaths[0]).childDataNodes(Collections.emptySet()).build();
+ if (xpaths.length > 1) {
+ final String[] xPathsDescendant = Arrays.copyOfRange(xpaths, 1, xpaths.length);
+ final DataNode childDataNode = createDataNodeTree(xPathsDescendant);
+ dataNode.setChildDataNodes(ImmutableSet.of(childDataNode));
+ }
+ return dataNode;
}
}
diff --git a/cps-ri/src/test/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceTest.java b/cps-ri/src/test/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceTest.java
index 050ee265a..0705fc4e6 100755
--- a/cps-ri/src/test/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceTest.java
+++ b/cps-ri/src/test/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceTest.java
@@ -50,10 +50,8 @@ import org.onap.cps.spi.repository.YangResourceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;
-import org.springframework.test.context.jdbc.SqlGroup;
import org.springframework.test.context.junit4.SpringRunner;
-
@RunWith(SpringRunner.class)
@SpringBootTest
public class CpsModulePersistenceServiceTest {
diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql
index c50f59530..05f5bfe45 100644
--- a/cps-ri/src/test/resources/data/fragment.sql
+++ b/cps-ri/src/test/resources/data/fragment.sql
@@ -8,6 +8,9 @@ INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES
(3001, 'ANCHOR-001', 1001, 2001);
INSERT INTO FRAGMENT (ID, XPATH, ANCHOR_ID, PARENT_ID, DATASPACE_ID) VALUES
- (3001, '/parent', 3001, null, 1001),
- (3002, '/parent/child', 3001, 3001, 1001),
- (3003, '/parent/child/grandchild', 3001, 3002, 1001); \ No newline at end of file
+ (4001, '/parent-1', 3001, null, 1001),
+ (4002, '/parent-2', 3001, null, 1001),
+ (4003, '/parent-3', 3001, null, 1001),
+ (4004, '/parent-1/child-1', 3001, 4001, 1001),
+ (4005, '/parent-2/child-2', 3001, 4002, 1001),
+ (4006, '/parent-1/child-1/grandchild-1', 3001, 4004, 1001); \ No newline at end of file
diff --git a/cps-service/pom.xml b/cps-service/pom.xml
index 642d76451..fc4ca1209 100644
--- a/cps-service/pom.xml
+++ b/cps-service/pom.xml
@@ -38,6 +38,14 @@
<artifactId>lombok</artifactId>
</dependency>
<dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-cache</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.github.ben-manes.caffeine</groupId>
+ <artifactId>caffeine</artifactId>
+ </dependency>
+ <dependency>
<!-- For logging -->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
@@ -64,6 +72,16 @@
<scope>test</scope>
</dependency>
<dependency>
+ <groupId>org.spockframework</groupId>
+ <artifactId>spock-spring</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<scope>test</scope>
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
index eac28a9f0..427ddd6c6 100644
--- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
@@ -20,7 +20,6 @@
package org.onap.cps.api.impl;
import java.util.Map;
-import org.checkerframework.checker.nullness.qual.NonNull;
import org.onap.cps.api.CpsModuleService;
import org.onap.cps.spi.CascadeDeleteAllowed;
import org.onap.cps.spi.CpsModulePersistenceService;
@@ -28,34 +27,32 @@ import org.onap.cps.spi.model.SchemaSet;
import org.onap.cps.yang.YangTextSchemaSourceSet;
import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Service;
-@Component("CpsModuleServiceImpl")
+@Service("CpsModuleServiceImpl")
public class CpsModuleServiceImpl implements CpsModuleService {
@Autowired
private CpsModulePersistenceService cpsModulePersistenceService;
+ @Autowired
+ private YangTextSchemaSourceSetCache yangTextSchemaSourceSetCache;
+
@Override
public void createSchemaSet(final String dataspaceName, final String schemaSetName,
- final Map<String, String> yangResourcesNameToContentMap) {
-
- YangTextSchemaSourceSetBuilder.validate(yangResourcesNameToContentMap);
- cpsModulePersistenceService
- .storeSchemaSet(dataspaceName, schemaSetName, yangResourcesNameToContentMap);
+ final Map<String, String> yangResourcesNameToContentMap) {
+ final YangTextSchemaSourceSet yangTextSchemaSourceSet
+ = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap);
+ cpsModulePersistenceService.storeSchemaSet(dataspaceName, schemaSetName, yangResourcesNameToContentMap);
+ yangTextSchemaSourceSetCache.updateCache(dataspaceName, schemaSetName, yangTextSchemaSourceSet);
}
@Override
public SchemaSet getSchemaSet(final String dataspaceName, final String schemaSetName) {
- final Map<String, String> yangResourceNameToContent =
- cpsModulePersistenceService.getYangSchemaResources(dataspaceName, schemaSetName);
- final YangTextSchemaSourceSet yangTextSchemaSourceSet = YangTextSchemaSourceSetBuilder
- .of(yangResourceNameToContent);
- return SchemaSet.builder()
- .name(schemaSetName)
- .dataspaceName(dataspaceName)
- .moduleReferences(yangTextSchemaSourceSet.getModuleReferences())
- .build();
+ final YangTextSchemaSourceSet yangTextSchemaSourceSet = yangTextSchemaSourceSetCache
+ .get(dataspaceName, schemaSetName);
+ return SchemaSet.builder().name(schemaSetName).dataspaceName(dataspaceName)
+ .moduleReferences(yangTextSchemaSourceSet.getModuleReferences()).build();
}
@Override
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java b/cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java
new file mode 100644
index 000000000..af16727f1
--- /dev/null
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/YangTextSchemaSourceSetCache.java
@@ -0,0 +1,70 @@
+package org.onap.cps.api.impl;
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2020 Pantheon.tech
+ * ================================================================================
+ * 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 com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.Map;
+import org.onap.cps.spi.CpsModulePersistenceService;
+import org.onap.cps.yang.YangTextSchemaSourceSet;
+import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheConfig;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+/**
+ * Provides cached YangTextSchemaSourceSet.
+ */
+@Service
+@CacheConfig(cacheNames = {"yangSchema"})
+public class YangTextSchemaSourceSetCache {
+
+ @Autowired
+ private CpsModulePersistenceService cpsModulePersistenceService;
+
+ /**
+ * Cache YangTextSchemaSourceSet.
+ *
+ * @param dataspaceName dataspace name
+ * @param schemaSetName schema set name
+ * @return YangTextSchemaSourceSet
+ */
+ @Cacheable(key = "#p0.concat('-').concat(#p1)")
+ public YangTextSchemaSourceSet get(final String dataspaceName, final String schemaSetName) {
+ final Map<String, String> yangResourceNameToContent =
+ cpsModulePersistenceService.getYangSchemaResources(dataspaceName, schemaSetName);
+ return YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent);
+ }
+
+ /**
+ * Updates cache YangTextSchemaSourceSet.
+ *
+ * @param dataspaceName dataspace name
+ * @param schemaSetName schema set name
+ * @param yangTextSchemaSourceSet yangTextSchemaSourceSet
+ * @return YangTextSchemaSourceSet
+ */
+ @CachePut(key = "#p0.concat('-').concat(#p1)")
+ @CanIgnoreReturnValue
+ public YangTextSchemaSourceSet updateCache(final String dataspaceName, final String schemaSetName,
+ final YangTextSchemaSourceSet yangTextSchemaSourceSet) {
+ return yangTextSchemaSourceSet;
+ }
+}
diff --git a/cps-service/src/main/java/org/onap/cps/config/CacheConfig.java b/cps-service/src/main/java/org/onap/cps/config/CacheConfig.java
new file mode 100644
index 000000000..4441e4f2a
--- /dev/null
+++ b/cps-service/src/main/java/org/onap/cps/config/CacheConfig.java
@@ -0,0 +1,29 @@
+package org.onap.cps.config;
+
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2020 Pantheon.tech
+ * ================================================================================
+ * 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 org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableCaching
+public class CacheConfig {
+
+} \ No newline at end of file
diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
index 50ece0e27..d59fa4746 100644
--- a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
+++ b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
@@ -39,4 +39,15 @@ public interface CpsDataPersistenceService {
*/
void storeDataNode(@NonNull String dataspaceName, @NonNull String anchorName,
@NonNull DataNode dataNode);
+
+ /**
+ * Add a child to a Fragment.
+ *
+ * @param dataspaceName dataspace name
+ * @param anchorName anchor name
+ * @param parentXpath parent xpath
+ * @param dataNode dataNode
+ */
+ void addChildDataNode(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentXpath,
+ @NonNull DataNode dataNode);
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
index f380d106c..5f2168aeb 100644
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
@@ -21,23 +21,35 @@
package org.onap.cps.api.impl
import org.onap.cps.TestUtils
-import org.onap.cps.spi.CascadeDeleteAllowed
-import org.onap.cps.spi.CpsModulePersistenceService;
+import org.onap.cps.api.CpsAdminService
+import org.onap.cps.spi.CpsModulePersistenceService
import org.onap.cps.spi.exceptions.ModelValidationException
import org.onap.cps.spi.model.ModuleReference
+import org.spockframework.spring.SpringBean
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.cache.CacheManager
+import org.springframework.cache.caffeine.CaffeineCacheManager
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
import spock.lang.Unroll
import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED
import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED
+@SpringBootTest
+@ComponentScan("org.onap.cps")
+@ContextConfiguration(classes = CpsModuleServiceImplSpec.class)
class CpsModuleServiceImplSpec extends Specification {
- def mockModuleStoreService = Mock(CpsModulePersistenceService)
- def objectUnderTest = new CpsModuleServiceImpl()
-
- def setup() {
- objectUnderTest.cpsModulePersistenceService = mockModuleStoreService
- }
+ @SpringBean
+ CpsModulePersistenceService mockModuleStoreService = Mock()
+ @SpringBean
+ CpsAdminService mockCpsAdminService = Mock()
+ @Autowired
+ CpsModuleServiceImpl objectUnderTest = new CpsModuleServiceImpl()
+ @SpringBean
+ CacheManager cacheManager = new CaffeineCacheManager("yangSchema");
def 'Create schema set'() {
given: 'Valid yang resource as name-to-content map'
@@ -69,6 +81,17 @@ class CpsModuleServiceImplSpec extends Specification {
result.getModuleReferences().contains(new ModuleReference('stores', 'org:onap:ccsdk:sample', '2020-09-15'))
}
+ def 'Schema set caching.'() {
+ given: 'an schema set'
+ def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang')
+ when: 'get schema set method is invoked twice'
+ 2.times {
+ objectUnderTest.getSchemaSet('someDataspace', 'someSchemaSet')
+ }
+ then: 'the persistency service called only once'
+ 1 * mockModuleStoreService.getYangSchemaResources('someDataspace', 'someSchemaSet') >> yangResourcesNameToContentMap
+ }
+
@Unroll
def 'Delete set by name and dataspace with #cascadeDeleteOption.'(){
when: 'schema set deletion is requested'
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
index 22dc39ad9..d6751bb4e 100755
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
@@ -24,13 +24,14 @@ import org.onap.cps.TestUtils
import org.onap.cps.spi.CpsModulePersistenceService
import spock.lang.Specification
-
class E2ENetworkSliceSpec extends Specification {
def mockModuleStoreService = Mock(CpsModulePersistenceService)
+ def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
def objectUnderTest = new CpsModuleServiceImpl()
def setup() {
objectUnderTest.cpsModulePersistenceService = mockModuleStoreService
+ objectUnderTest.yangTextSchemaSourceSetCache = mockYangTextSchemaSourceSetCache
}
def 'E2E model can be parsed by CPS.'() {