Fetch data filtered by Search criteria 43/122943/12
authorRenu Kumari <renu.kumari@bell.ca>
Wed, 28 Jul 2021 04:07:41 +0000 (00:07 -0400)
committerRenu Kumari <renu.kumari@bell.ca>
Wed, 4 Aug 2021 12:37:43 +0000 (08:37 -0400)
Implemented Service and repository layer

Issue-ID: CPS-375
Signed-off-by: Renu Kumari <renu.kumari@bell.ca>
Change-Id: Iedac0e48b2391a60f3eb9ab710ccdff0c9185407

12 files changed:
docs/api/swagger/openapi.yml
src/main/java/org/onap/cps/temporal/domain/SearchCriteria.java [new file with mode: 0644]
src/main/java/org/onap/cps/temporal/repository/NetworkDataQueryRepository.java [new file with mode: 0644]
src/main/java/org/onap/cps/temporal/repository/NetworkDataRepository.java
src/main/java/org/onap/cps/temporal/repository/NetworkDataRepositoryImpl.java [new file with mode: 0644]
src/main/java/org/onap/cps/temporal/service/NetworkDataService.java
src/main/java/org/onap/cps/temporal/service/NetworkDataServiceImpl.java
src/test/groovy/org/onap/cps/temporal/domain/SearchCriteriaSpec.groovy [new file with mode: 0644]
src/test/groovy/org/onap/cps/temporal/repository/NetworkDataRepositoryImplSpec.groovy [new file with mode: 0644]
src/test/groovy/org/onap/cps/temporal/repository/NetworkDataRepositorySpec.groovy
src/test/groovy/org/onap/cps/temporal/service/NetworkDataServiceImplSpec.groovy
src/test/resources/data/network-data-changes.sql [new file with mode: 0644]

index 9a133a3..cde25a8 100644 (file)
@@ -51,7 +51,7 @@ paths:
           required: true
           schema:
             type: string
-        - $ref: '#/components/parameters/after'
+        - $ref: '#/components/parameters/observedTimestampAfter'
         - $ref: '#/components/parameters/simplePayloadFilter'
         - $ref: '#/components/parameters/pointInTime'
         - $ref: '#/components/parameters/pageNumber'
@@ -97,7 +97,7 @@ paths:
           required: true
           schema:
             type: string
-        - $ref: '#/components/parameters/after'
+        - $ref: '#/components/parameters/observedTimestampAfter'
         - $ref: '#/components/parameters/simplePayloadFilter'
         - $ref: '#/components/parameters/pointInTime'
         - $ref: '#/components/parameters/pageNumber'
@@ -136,10 +136,10 @@ components:
       required: true
       schema:
         type: string
-    after:
-      name: after
+    observedTimestampAfter:
+      name: observedTimestampAfter
       in: query
-      description: Fetch data after <br/> Format - 'yyyy-MM-ddTHH:mm:ss.SSSZ'
+      description: Fetch data with observed timestamp after <br/> Format - 'yyyy-MM-ddTHH:mm:ss.SSSZ'
       required: false
       schema:
         type: string
@@ -184,7 +184,7 @@ components:
       required: false
       schema:
         type: string
-        default: timestamp:desc
+        default: observed_timestamp:desc
       description: "Sort by timestamp in 'asc' or 'desc' order. Supported values: <br/> timestamp:desc<br/>timestamp:asc"
   responses:
     BadRequest:
diff --git a/src/main/java/org/onap/cps/temporal/domain/SearchCriteria.java b/src/main/java/org/onap/cps/temporal/domain/SearchCriteria.java
new file mode 100644 (file)
index 0000000..8188d84
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2021 Bell Canada.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.temporal.domain;
+
+import java.time.OffsetDateTime;
+import javax.validation.constraints.NotNull;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Direction;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Builder(builderClassName = "Builder")
+public class SearchCriteria {
+
+    private OffsetDateTime createdBefore;
+    private OffsetDateTime observedAfter;
+    private String dataspaceName;
+    private String anchorName;
+    private String schemaSetName;
+    private Pageable pageable;
+    private String simplePayloadFilter;
+
+    public static class Builder {
+
+        private Sort sort = Sort.by(Direction.DESC, "observed_timestamp");
+        private OffsetDateTime createdBefore = OffsetDateTime.now();
+
+        public Builder pagination(final int pageNumber, final int pageSize) {
+            pageable = PageRequest.of(pageNumber, pageSize);
+            return this;
+        }
+
+        public Builder sort(final @NotNull Sort sort) {
+            this.sort = sort;
+            return this;
+        }
+
+        /**
+         * Validates the state before building search criteria.
+         *
+         * @return SearchCriteria searchCriteria
+         */
+        public SearchCriteria build() {
+
+            if (StringUtils.isEmpty(anchorName) && StringUtils.isEmpty(schemaSetName)) {
+                throw new IllegalStateException(
+                    "Either anchorName or schemaSetName must be provided");
+            }
+
+            if (StringUtils.isEmpty(dataspaceName)) {
+                throw new IllegalStateException("Dataspace is mandatory");
+            }
+
+            if (pageable == null) {
+                throw new IllegalStateException("Pageable is mandatory");
+            }
+
+            final var searchCriteria = new SearchCriteria();
+            searchCriteria.createdBefore = createdBefore;
+            searchCriteria.observedAfter = observedAfter;
+            searchCriteria.dataspaceName = dataspaceName;
+            searchCriteria.anchorName = anchorName;
+            searchCriteria.schemaSetName = schemaSetName;
+            searchCriteria.pageable = ((PageRequest) pageable).withSort(sort);
+            searchCriteria.simplePayloadFilter = simplePayloadFilter;
+            return searchCriteria;
+        }
+
+    }
+
+}
+
+
diff --git a/src/main/java/org/onap/cps/temporal/repository/NetworkDataQueryRepository.java b/src/main/java/org/onap/cps/temporal/repository/NetworkDataQueryRepository.java
new file mode 100644 (file)
index 0000000..12d3d68
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2021 Bell Canada.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.temporal.repository;
+
+import org.onap.cps.temporal.domain.NetworkData;
+import org.onap.cps.temporal.domain.SearchCriteria;
+import org.springframework.data.domain.Slice;
+
+public interface NetworkDataQueryRepository {
+
+    Slice<NetworkData> findBySearchCriteria(SearchCriteria searchCriteria);
+
+}
index 2e9f34b..c0f4fc9 100644 (file)
@@ -13,6 +13,8 @@
  * 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=========================================================
  */
 
@@ -24,5 +26,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.stereotype.Repository;
 
 @Repository
-public interface NetworkDataRepository extends JpaRepository<NetworkData, NetworkDataId> {
+public interface NetworkDataRepository extends JpaRepository<NetworkData, NetworkDataId>,
+    NetworkDataQueryRepository {
 }
diff --git a/src/main/java/org/onap/cps/temporal/repository/NetworkDataRepositoryImpl.java b/src/main/java/org/onap/cps/temporal/repository/NetworkDataRepositoryImpl.java
new file mode 100644 (file)
index 0000000..548f973
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2021 Bell Canada.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.temporal.repository;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.onap.cps.temporal.domain.NetworkData;
+import org.onap.cps.temporal.domain.SearchCriteria;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.SliceImpl;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@Slf4j
+public class NetworkDataRepositoryImpl implements NetworkDataQueryRepository {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    /*
+    Slice is the response type instead of List<NetworkData> to provide the information if next set of data is available.
+    To identify if next slice is available, the getDatNetworkDataList fetches one record extra ( n+1).
+    If ( n +1) records are fetched, it means that the next slice exist, otherwise it does not.
+     */
+    @Override
+    public Slice<NetworkData> findBySearchCriteria(final SearchCriteria searchCriteria) {
+
+        final var searchCriteriaQueryBuilder = new SearchCriteriaQueryBuilder(searchCriteria);
+        searchCriteriaQueryBuilder.buildQuery();
+
+        final List<NetworkData> data = getNetworkDataList(searchCriteriaQueryBuilder.getDataNativeQuery(),
+            searchCriteriaQueryBuilder.getQueryParameters(), searchCriteria.getPageable());
+
+        final boolean hasNextSlice = data.size() > searchCriteria.getPageable().getPageSize();
+        final List<NetworkData> sliceData = new ArrayList<>(data);
+        if (hasNextSlice) {
+            sliceData.remove(searchCriteria.getPageable().getPageSize());
+        }
+
+        return new SliceImpl<>(sliceData, searchCriteria.getPageable(), hasNextSlice);
+    }
+
+    private List<NetworkData> getNetworkDataList(final String nativeDataQuery,
+        final Map<String, Object> queryParameters, final Pageable pageable) {
+        final var dataQuery = entityManager.createNativeQuery(nativeDataQuery, NetworkData.class);
+        queryParameters.forEach(dataQuery::setParameter);
+        dataQuery.setFirstResult(Math.toIntExact(pageable.getOffset()));
+        dataQuery.setMaxResults(pageable.getPageSize() + 1);
+        return dataQuery.getResultList();
+    }
+
+    private static class SearchCriteriaQueryBuilder {
+
+        @Getter
+        private Map<String, Object> queryParameters = new HashMap<>();
+        private StringBuilder queryBuilder = new StringBuilder();
+
+        private String dataQuery;
+
+        private final SearchCriteria searchCriteria;
+
+        SearchCriteriaQueryBuilder(final SearchCriteria searchCriteria) {
+            this.searchCriteria = searchCriteria;
+        }
+
+        private void buildQuery() {
+
+            queryBuilder.append("SELECT * FROM network_data nd WHERE dataspace = :dataspace ");
+            queryParameters.put("dataspace", searchCriteria.getDataspaceName());
+
+            addAnchorCondition();
+            addSchemaSetCondition();
+            addObservedAfterCondition();
+            addSimplePayloadCondition();
+            addCreatedBeforeCondition();
+            addOrderBy();
+            dataQuery = queryBuilder.toString();
+
+        }
+
+
+        private void addSchemaSetCondition() {
+            if (!StringUtils.isEmpty(searchCriteria.getSchemaSetName())) {
+                queryBuilder.append(" AND schema_set = :schemaSetName ");
+                queryParameters.put("schemaSetName", searchCriteria.getSchemaSetName());
+            }
+        }
+
+        private void addAnchorCondition() {
+            if (!StringUtils.isEmpty(searchCriteria.getAnchorName())) {
+                queryBuilder.append(" AND anchor = :anchorName");
+                queryParameters.put("anchorName", searchCriteria.getAnchorName());
+            }
+        }
+
+        private void addSimplePayloadCondition() {
+            if (!StringUtils.isEmpty(searchCriteria.getSimplePayloadFilter())) {
+                queryBuilder.append(" AND payload @> :simplePayloadFilter\\:\\:jsonb ");
+                queryParameters.put("simplePayloadFilter", searchCriteria.getSimplePayloadFilter());
+            }
+        }
+
+        private void addCreatedBeforeCondition() {
+            if (searchCriteria.getCreatedBefore() != null) {
+                queryBuilder.append(" AND created_timestamp <= :createdBefore");
+                queryParameters.put("createdBefore", searchCriteria.getCreatedBefore());
+            }
+        }
+
+        private void addObservedAfterCondition() {
+            if (searchCriteria.getObservedAfter() != null) {
+                queryBuilder.append(" AND observed_timestamp >= :observedAfter");
+                queryParameters.put("observedAfter", searchCriteria.getObservedAfter());
+            }
+        }
+
+        private void addOrderBy() {
+            final var sortBy = searchCriteria.getPageable().getSort();
+            queryBuilder.append(" ORDER BY ");
+            final String orderByQuery = sortBy.stream().map(order -> {
+                final var direction = order.isAscending() ? "asc" : "desc";
+                return order.getProperty() + " " + direction;
+            }).collect(Collectors.joining(","));
+            queryBuilder.append(orderByQuery);
+        }
+
+        String getDataNativeQuery() {
+            return dataQuery;
+        }
+
+    }
+
+
+}
+
index 509e470..261f05e 100644 (file)
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
  * ============LICENSE_END=========================================================
  */
 
 package org.onap.cps.temporal.service;
 
 import org.onap.cps.temporal.domain.NetworkData;
+import org.onap.cps.temporal.domain.SearchCriteria;
+import org.springframework.data.domain.Slice;
 
 public interface NetworkDataService {
 
@@ -28,4 +32,6 @@ public interface NetworkDataService {
      * @param networkData the network data to be stored
      */
     NetworkData addNetworkData(NetworkData networkData);
+
+    Slice<NetworkData> searchNetworkData(SearchCriteria searchCriteria);
 }
index 687ba85..7c2f999 100644 (file)
@@ -13,6 +13,8 @@
  * 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=========================================================
  */
 
@@ -22,7 +24,9 @@ import java.util.Optional;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.temporal.domain.NetworkData;
 import org.onap.cps.temporal.domain.NetworkDataId;
+import org.onap.cps.temporal.domain.SearchCriteria;
 import org.onap.cps.temporal.repository.NetworkDataRepository;
+import org.springframework.data.domain.Slice;
 import org.springframework.stereotype.Service;
 
 /**
@@ -44,13 +48,18 @@ public class NetworkDataServiceImpl implements NetworkDataService {
         if (savedNetworkData.getCreatedTimestamp() == null) {
             // Data already exists and can not be inserted
             final var id =
-                    new NetworkDataId(
-                            networkData.getObservedTimestamp(), networkData.getDataspace(), networkData.getAnchor());
+                new NetworkDataId(
+                    networkData.getObservedTimestamp(), networkData.getDataspace(), networkData.getAnchor());
             final Optional<NetworkData> existingNetworkData = networkDataRepository.findById(id);
             throw new ServiceException(
-                    "Failed to create network data. It already exists: " + (existingNetworkData.orElse(null)));
+                "Failed to create network data. It already exists: " + (existingNetworkData.orElse(null)));
         }
         return savedNetworkData;
     }
 
+    @Override
+    public Slice<NetworkData> searchNetworkData(final SearchCriteria searchCriteria) {
+        return networkDataRepository.findBySearchCriteria(searchCriteria);
+    }
+
 }
diff --git a/src/test/groovy/org/onap/cps/temporal/domain/SearchCriteriaSpec.groovy b/src/test/groovy/org/onap/cps/temporal/domain/SearchCriteriaSpec.groovy
new file mode 100644 (file)
index 0000000..d7b6d1f
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2021 Bell Canada.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.cps.temporal.domain
+
+import org.springframework.data.domain.Sort
+import spock.lang.Specification
+
+import java.time.OffsetDateTime
+
+class SearchCriteriaSpec extends Specification {
+
+    def myDataspace = 'my-dataspace'
+    def myAnchorName = 'my-anchor'
+    def myschemaSetName = 'my-schemaset'
+
+
+    def 'Search Criteria has default values if not provided.'() {
+        def myPayloadFilter = '{"status": "down"}'
+        when: 'search criteria is created'
+            def searchCriteria = SearchCriteria.builder()
+                .dataspaceName(myDataspace)
+                .schemaSetName(myschemaSetName)
+                .pagination(0, 10)
+                .simplePayloadFilter(myPayloadFilter)
+                .build()
+
+        then: 'search criteria has default value for sort'
+            searchCriteria.getPageable().getSort() == Sort.by(Sort.Direction.DESC, 'observed_timestamp')
+        and: 'created before has almost current time as default value'
+            OffsetDateTime.now().minusMinutes(5).isBefore(searchCriteria.getCreatedBefore())
+        and: 'contains the provided value to builder'
+            searchCriteria.getDataspaceName() == myDataspace
+            searchCriteria.getSchemaSetName() == myschemaSetName
+            searchCriteria.getSimplePayloadFilter() == myPayloadFilter
+            searchCriteria.getPageable().getPageNumber() == 0
+            searchCriteria.getPageable().getPageSize() == 10
+
+    }
+
+    def 'Search Criteria with the provided values.'() {
+
+        given: 'sort by parameter'
+            def sortBy = Sort.by(Sort.Direction.ASC, 'observed_timestamp')
+        and: 'data created one day ago'
+            def lastDayAsCreatedBefore = OffsetDateTime.now().minusDays(1)
+        and: 'observed timestamp'
+            def nowAsObservedAfter = OffsetDateTime.now()
+
+        when: 'search criteria is created'
+            def searchCriteria = SearchCriteria.builder()
+                .dataspaceName(myDataspace)
+                .schemaSetName(myschemaSetName)
+                .anchorName(myAnchorName)
+                .pagination(0, 10)
+                .sort(sortBy)
+                .observedAfter(nowAsObservedAfter)
+                .createdBefore(lastDayAsCreatedBefore)
+                .build()
+
+        then: 'search criteria has expected value'
+            with(searchCriteria) {
+                dataspaceName == myDataspace
+                schemaSetName == myschemaSetName
+                anchorName == myAnchorName
+                observedAfter == nowAsObservedAfter
+                createdBefore == lastDayAsCreatedBefore
+                pageable.getPageNumber() == 0
+                pageable.getPageSize() == 10
+                pageable.getSort() == sortBy
+            }
+    }
+
+    def 'Error handling: missing dataspace.'() {
+        when: 'search criteria is created without dataspace'
+            SearchCriteria.builder()
+                .anchorName(myAnchorName)
+                .pagination(0, 10)
+                .build()
+        then: 'exception is thrown'
+            thrown(IllegalStateException)
+    }
+
+    def 'Error handling: missing both schemaset and anchor.'() {
+        when: 'search criteria is created without schemaset and anchor'
+            SearchCriteria.builder()
+                .dataspaceName(myDataspace)
+                .pagination(0, 10)
+                .build()
+        then: 'exception is thrown'
+            thrown(IllegalStateException)
+    }
+
+    def 'Error handling: missing pagination.'() {
+        when: 'search criteria is created without pagination'
+            SearchCriteria.builder()
+                .dataspaceName(myDataspace)
+                .anchorName(myAnchorName)
+                .build()
+        then: 'exception is thrown'
+            thrown(IllegalStateException)
+    }
+
+    def 'Error Handling: sort must be not null.'() {
+        when: 'search criteria is created without sorting information'
+            SearchCriteria.builder()
+                .dataspaceName(myDataspace)
+                .anchorName(myAnchorName)
+                .pagination(0, 1)
+                .sort(null)
+                .build()
+        then: 'exception is thrown'
+            thrown(IllegalArgumentException)
+    }
+
+}
diff --git a/src/test/groovy/org/onap/cps/temporal/repository/NetworkDataRepositoryImplSpec.groovy b/src/test/groovy/org/onap/cps/temporal/repository/NetworkDataRepositoryImplSpec.groovy
new file mode 100644 (file)
index 0000000..a5cc721
--- /dev/null
@@ -0,0 +1,206 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2021 Bell Canada.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+  * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.temporal.repository
+
+import org.onap.cps.temporal.domain.NetworkData
+import org.onap.cps.temporal.domain.SearchCriteria
+import org.onap.cps.temporal.repository.containers.TimescaleContainer
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
+import org.springframework.data.domain.PageRequest
+import org.springframework.data.domain.Slice
+import org.springframework.data.domain.Sort
+import org.springframework.test.context.jdbc.Sql
+import org.testcontainers.spock.Testcontainers
+import org.springframework.test.annotation.Rollback
+import spock.lang.Shared
+import spock.lang.Specification
+import java.time.LocalDateTime
+import java.time.OffsetDateTime
+import java.time.ZoneOffset
+import java.time.format.DateTimeFormatter
+
+/**
+ * Test specification for network data repository.
+ */
+@Testcontainers
+@DataJpaTest
+@Rollback(false)
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+class NetworkDataRepositoryImplSpec extends Specification {
+
+    static final String RELOAD_DATA_FOR_SEARCHING = '/data/network-data-changes.sql'
+
+    @Shared
+    DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss.SSS')
+
+    def queryDataspaceName = 'DATASPACE-01'
+    @Shared
+    def querySchemaSetName = 'SCHEMA-SET-01'
+    @Shared
+    def queryAnchorName = 'ANCHOR-01'
+
+    @Shared
+    def observedAscSortOrder = new Sort.Order(Sort.Direction.ASC, 'observed_timestamp')
+    @Shared
+    def observedDescSortOrder = new Sort.Order(Sort.Direction.DESC, 'observed_timestamp')
+    @Shared
+    def anchorAscSortOrder = new Sort.Order(Sort.Direction.ASC, 'anchor')
+
+    @Autowired
+    NetworkDataRepository networkDataRepository
+
+    @Shared
+    TimescaleContainer databaseTestContainer = TimescaleContainer.getInstance()
+
+    @Sql([RELOAD_DATA_FOR_SEARCHING])
+    def 'Query: pagination for #scenario'() {
+        given: 'search criteria'
+            def searchCriteria = (new SearchCriteria.Builder())
+                .dataspaceName(queryDataspaceName)
+                .anchorName(queryAnchorName)
+                .pagination(pageNumber, 1)
+                .build()
+        when: 'data is fetched'
+            Slice<NetworkData> result = networkDataRepository.findBySearchCriteria(searchCriteria)
+        then: 'result has expected values'
+            result.getNumberOfElements() == 1L
+            NetworkData networkData = result.getContent().get(0);
+            networkData.getAnchor() == queryAnchorName
+            networkData.getDataspace() == queryDataspaceName
+            networkData.getSchemaSet() == querySchemaSetName
+        and: ' correct pagination details'
+            result.hasNext() ? result.nextPageable() : null == expectedNextPageable
+            result.hasPrevious() ? result.previousPageable() : null == expectedPreviousPageable
+        where:
+            scenario      | pageNumber || expectedPreviousPageable | expectedNextPageable
+            'first Page'  | 0          || null                     | PageRequest.of(1, 1)
+            'middle Page' | 1          || PageRequest.of(0, 1)     | PageRequest.of(2, 1)
+            'last Page'   | 2          || PageRequest.of(1, 1)     | null
+    }
+
+    @Sql([RELOAD_DATA_FOR_SEARCHING])
+    def 'Query: filter by observed after.'() {
+        given: 'observed after date'
+            def observedAfter = getOffsetDateDate('2021-07-22 01:00:01.000')
+        and: 'search criteria'
+            def searchCriteria = (new SearchCriteria.Builder())
+                .dataspaceName(queryDataspaceName)
+                .anchorName(queryAnchorName)
+                .observedAfter(observedAfter)
+                .pagination(0, 4)
+                .build()
+        when: 'data is fetched'
+            Slice<NetworkData> result = networkDataRepository.findBySearchCriteria(searchCriteria)
+        then: 'result have expected number of record'
+            result.getNumberOfElements() == 2L
+        and: 'each record has observed timestamp on or after the provided value'
+            for (NetworkData data : result.getContent()) {
+                assert data.getObservedTimestamp().isAfter(observedAfter) || data.getObservedTimestamp().isEqual(observedAfter)
+                assert data.getAnchor() == queryAnchorName
+                assert data.getDataspace() == queryDataspaceName
+            }
+    }
+
+    @Sql([RELOAD_DATA_FOR_SEARCHING])
+    def 'Query: filter by created before.'() {
+        given: 'created before date'
+            def createdBefore = getOffsetDateDate('2021-07-22 23:00:01.000')
+        and: 'search criteria'
+            def searchCriteria = (new SearchCriteria.Builder())
+                .dataspaceName(queryDataspaceName)
+                .anchorName(queryAnchorName)
+                .createdBefore(createdBefore)
+                .pagination(0, 4)
+                .build()
+        when: 'data is fetched'
+            Slice<NetworkData> result = networkDataRepository.findBySearchCriteria(searchCriteria)
+        then: 'result have expected number of record'
+            result.getNumberOfElements() == 2L
+        and: 'each record has observed timestamp on or after the provided value'
+            for (NetworkData data : result.getContent()) {
+                assert data.getCreatedTimestamp().isBefore(createdBefore) || data.getCreatedTimestamp().isEqual(createdBefore)
+                assert data.getAnchor() == queryAnchorName
+                assert data.getDataspace() == queryDataspaceName
+            }
+    }
+
+    @Sql([RELOAD_DATA_FOR_SEARCHING])
+    def 'Query: sort data by #scenario.'() {
+        given: 'search criteria'
+            def searchCriteria = (new SearchCriteria.Builder())
+                .dataspaceName(queryDataspaceName)
+                .schemaSetName(querySchemaSetName)
+                .sort(sortOrder)
+                .pagination(0, 4)
+                .build()
+        when: 'data is fetched'
+            Slice<NetworkData> result = networkDataRepository.findBySearchCriteria(searchCriteria)
+        then: 'result has expected values'
+            result.getNumberOfElements() == 4L
+            with(result.getContent().get(0)) {
+                dataspace == queryDataspaceName
+                schemaSet == querySchemaSetName
+                anchor == expectedAnchorName
+                observedTimestamp == getOffsetDateDate(expectedObservedTimestamp)
+            }
+        where:
+            scenario                      | sortOrder                                          || expectedObservedTimestamp | expectedAnchorName
+            'observed timestamp asc'      | Sort.by(observedAscSortOrder)                      || '2021-07-22 00:00:01.000' | 'ANCHOR-01'
+            'observed timestamp asc'      | Sort.by(observedDescSortOrder)                     || '2021-07-24 00:00:01.000' | 'ANCHOR-02'
+            'anchor asc, ' +
+                'observed timestamp desc' | Sort.by(anchorAscSortOrder, observedDescSortOrder) || '2021-07-23 00:00:01.000' | 'ANCHOR-01'
+
+    }
+
+    @Sql([RELOAD_DATA_FOR_SEARCHING])
+    def 'Query: filter by payload.'() {
+        def dataspaceName = 'DATASPACE-02'
+        given: 'search criteria'
+            def searchCriteria = (new SearchCriteria.Builder())
+                .dataspaceName(dataspaceName)
+                .schemaSetName(querySchemaSetName)
+                .simplePayloadFilter(simplePayloadFilter)
+                .pagination(0, 4)
+                .build()
+        when: 'data is fetched'
+            Slice<NetworkData> result = networkDataRepository.findBySearchCriteria(searchCriteria)
+        then: 'result has expected values'
+            result.getNumberOfElements() == expectedRecordsCount
+            with(result.getContent().get(0)) {
+                dataspace == dataspaceName
+                schemaSet == querySchemaSetName
+                anchor == queryAnchorName
+            }
+        where:
+            simplePayloadFilter                    || expectedRecordsCount
+            '{"interfaces": [{"id": "01"}]}'       || 2L
+            '{"interfaces": [{"status": "down"}]}' || 1L
+
+    }
+
+    OffsetDateTime getOffsetDateDate(String dateTimeString) {
+        def localDateTime = LocalDateTime.parse(dateTimeString, ISO_TIMESTAMP_FORMATTER)
+        def localZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(localDateTime)
+        return OffsetDateTime.of(localDateTime, localZoneOffset)
+    }
+}
index 41f3f42..2c7fc5e 100644 (file)
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
  * ============LICENSE_END=========================================================
  */
 
 package org.onap.cps.temporal.repository
 
-
 import org.onap.cps.temporal.domain.NetworkData
 import org.onap.cps.temporal.repository.containers.TimescaleContainer
 import org.springframework.beans.factory.annotation.Autowired
@@ -29,28 +30,29 @@ import org.springframework.test.annotation.Rollback
 import org.springframework.test.context.transaction.TestTransaction
 import spock.lang.Shared
 import spock.lang.Specification
-
 import java.time.OffsetDateTime
 
 /**
  * Test specification for network data repository.
  */
 @Testcontainers
-@DataJpaTest @Rollback(false)
+@DataJpaTest
+@Rollback(false)
 @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
 class NetworkDataRepositorySpec extends Specification {
 
+    @Shared
     def observedTimestamp = OffsetDateTime.now()
-    def dataspaceName = 'TEST_DATASPACE'
-    def schemaSetName = 'TEST_SCHEMA_SET'
-    def anchorName = 'TEST_ANCHOR'
-    def payload = '{ "message": "Hello World!" }'
+    def myDataspaceName = 'MY_DATASPACE'
+    def mySchemaSetName = 'MY_SCHEMA_SET'
+    def myAnchorName = 'MY_ANCHOR'
+    def payload = '{"message": "Hello World!"}'
 
     @Autowired
     NetworkDataRepository networkDataRepository
 
-    def networkData = NetworkData.builder().observedTimestamp(observedTimestamp).dataspace(dataspaceName)
-            .schemaSet(schemaSetName).anchor(anchorName).payload(payload).build()
+    def networkData = NetworkData.builder().observedTimestamp(observedTimestamp).dataspace(myDataspaceName)
+        .schemaSet(mySchemaSetName).anchor(myAnchorName).payload(payload).build()
 
     @Shared
     TimescaleContainer databaseTestContainer = TimescaleContainer.getInstance()
@@ -71,4 +73,5 @@ class NetworkDataRepositorySpec extends Specification {
         and: ' the CreationTimestamp is ahead of ObservedTimestamp'
             savedData.getCreatedTimestamp() > networkData.getObservedTimestamp()
     }
+
 }
index 9847f54..c55c3c7 100644 (file)
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
  * ============LICENSE_END=========================================================
  */
 
 package org.onap.cps.temporal.service
 
 import org.onap.cps.temporal.domain.NetworkDataId
-
+import org.onap.cps.temporal.domain.SearchCriteria
+import org.springframework.data.domain.PageImpl
 import java.time.OffsetDateTime
 import org.onap.cps.temporal.domain.NetworkData
 import org.onap.cps.temporal.repository.NetworkDataRepository
@@ -50,7 +53,8 @@ class NetworkDataServiceImplSpec extends Specification {
     }
 
     def 'Add network data fails because already added'() {
-        given: 'network data repository is not able to create data it is asked to persist ' +
+        given:
+            'network data repository is not able to create data it is asked to persist ' +
                 'and reveals it with null created timestamp on network data entity'
             def persistedNetworkData = new NetworkData()
             persistedNetworkData.setCreatedTimestamp(null)
@@ -65,4 +69,23 @@ class NetworkDataServiceImplSpec extends Specification {
             thrown(ServiceException)
     }
 
+    def 'Query network data by search criteria.'() {
+        given: 'search criteria'
+            def searchCriteria = SearchCriteria.builder()
+                .dataspaceName('my-dataspaceName')
+                .schemaSetName('my-schemaset')
+                .pagination(0, 10)
+                .build()
+        and: 'response from repository'
+            def pageFromRepository = new PageImpl<>(Collections.emptyList(), searchCriteria.getPageable(), 10)
+            mockNetworkDataRepository.findBySearchCriteria(searchCriteria) >> pageFromRepository
+
+        when: 'search is executed'
+            def resultPage = objectUnderTest.searchNetworkData(searchCriteria)
+
+        then: 'data is fetched from repository and returned'
+            resultPage == pageFromRepository
+
+    }
+
 }
diff --git a/src/test/resources/data/network-data-changes.sql b/src/test/resources/data/network-data-changes.sql
new file mode 100644 (file)
index 0000000..ce15f19
--- /dev/null
@@ -0,0 +1,32 @@
+--Clear the data before inserting
+DELETE FROM NETWORK_DATA WHERE DATASPACE in ( 'DATASPACE-01', 'DATASPACE-02');
+COMMIT;
+
+-- Test pagination data
+-- Test created Before filter
+-- Test observed After Filter
+INSERT INTO NETWORK_DATA (OBSERVED_TIMESTAMP, DATASPACE, ANCHOR, SCHEMA_SET, PAYLOAD, CREATED_TIMESTAMP)
+VALUES
+('2021-07-22 00:00:01.000', 'DATASPACE-01', 'ANCHOR-01', 'SCHEMA-SET-01', '{ "status" : "up" }'::jsonb, '2021-07-22 23:00:01.000'),
+('2021-07-22 01:00:01.000', 'DATASPACE-01', 'ANCHOR-01', 'SCHEMA-SET-01', '{ "status" : "down" }'::jsonb, '2021-07-22 23:00:01.000'),
+('2021-07-23 00:00:01.000', 'DATASPACE-01', 'ANCHOR-01', 'SCHEMA-SET-01', '{ "status" : "up" }'::jsonb, '2021-07-23 23:00:01.000');
+
+-- Test sorting on multiple fields
+INSERT INTO NETWORK_DATA (OBSERVED_TIMESTAMP, DATASPACE, ANCHOR, SCHEMA_SET, PAYLOAD, CREATED_TIMESTAMP)
+VALUES
+('2021-07-24 00:00:01.000', 'DATASPACE-01', 'ANCHOR-02', 'SCHEMA-SET-01', '{ "status" : "up" }'::jsonb, '2021-07-24 23:00:01.000');
+
+
+-- Test simple payload filter on multiple field
+INSERT INTO NETWORK_DATA (OBSERVED_TIMESTAMP, DATASPACE, ANCHOR, SCHEMA_SET, PAYLOAD, CREATED_TIMESTAMP)
+VALUES
+('2021-07-24 00:00:01.000', 'DATASPACE-02', 'ANCHOR-01', 'SCHEMA-SET-01', '{ "interfaces": [ { "id" : "01", "status" : "up" } ]}'::jsonb, '2021-07-24 01:00:01.000'),
+('2021-07-24 01:00:01.000', 'DATASPACE-02', 'ANCHOR-01', 'SCHEMA-SET-01', '{ "interfaces": [ { "id" : "01", "status" : "down" } ]}'::jsonb, '2021-07-24 02:00:01.000'),
+('2021-07-24 02:00:01.000', 'DATASPACE-02', 'ANCHOR-01', 'SCHEMA-SET-01', '{ "interfaces": [ { "id" : "02", "status" : "up" } ]}'::jsonb, '2021-07-24 03:00:01.000'),
+('2021-07-24 03:00:01.000', 'DATASPACE-02', 'ANCHOR-01', 'SCHEMA-SET-01', '{ "interfaces": [ { "id" : "03", "status" : "up" } ]}'::jsonb, '2021-07-24 04:00:01.000');
+
+COMMIT;
+
+
+
+