diff options
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 ebb9a19e67..67b2863695 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 f427555aa1..c3b61541f6 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 128c56680b..49c3267dd9 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 c53d1a42a6..532a0ca848 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 b50a92662b..0f2ef20df7 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 2d4fe9bb01..540d6224aa 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 ba5aa4cac0..3e2bdec37e 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 Binary files differnew file mode 100644 index 0000000000..83f6963b18 --- /dev/null +++ b/cps-rest/src/test/resources/no-yang-files.zip diff --git a/cps-rest/src/test/resources/yang-files-multiple-sets.zip b/cps-rest/src/test/resources/yang-files-multiple-sets.zip Binary files differnew file mode 100644 index 0000000000..855e87bb27 --- /dev/null +++ b/cps-rest/src/test/resources/yang-files-multiple-sets.zip diff --git a/cps-rest/src/test/resources/yang-files-set.zip b/cps-rest/src/test/resources/yang-files-set.zip Binary files differnew file mode 100644 index 0000000000..09236ce474 --- /dev/null +++ b/cps-rest/src/test/resources/yang-files-set.zip 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 283d646825..c73b65ddd8 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 cac41ca9d5..b28beb42c9 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 4d44943963..6fc956c44d 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 d37d932aad..2735ca4898 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 db2941c19f..de0942c3c3 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 050ee265a5..0705fc4e61 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 c50f595306..05f5bfe455 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 642d76451d..fc4ca12097 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 eac28a9f09..427ddd6c6f 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 0000000000..af16727f19 --- /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 0000000000..4441e4f2a1 --- /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 50ece0e27b..d59fa47467 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 f380d106c7..5f2168aeb9 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 22dc39ad90..d6751bb4e2 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.'() {
|