aboutsummaryrefslogtreecommitdiffstats
path: root/src/main
diff options
context:
space:
mode:
authorRenu Kumari <renu.kumari@bell.ca>2021-08-10 16:43:04 -0400
committerRenu Kumari <renu.kumari@bell.ca>2021-08-19 07:13:57 -0400
commitea04c07ad990b5543766e95e234cae746bd1fbc1 (patch)
treec50008212dbf1471e859c0f9f681baa146a57c7c /src/main
parent25ed5a8a63e88edd0b9c8285b640839a6e93866a (diff)
Validate controller input and provide expected response
- validate endpoint input - set default values if it is missing in the optional fields - used hateoas for previous and next record link generation - use service layer in controller layer Issue-ID: CPS-449 Signed-off-by: Renu Kumari <renu.kumari@bell.ca> Change-Id: I7936cd9e8e7dead3b5650b421bb12f10d14ffa9b
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/org/onap/cps/temporal/controller/event/model/CpsDataUpdatedEventMapper.java11
-rw-r--r--src/main/java/org/onap/cps/temporal/controller/rest/QueryController.java93
-rw-r--r--src/main/java/org/onap/cps/temporal/controller/rest/QueryExceptionHandler.java73
-rw-r--r--src/main/java/org/onap/cps/temporal/controller/rest/QueryResponseFactory.java170
-rw-r--r--src/main/java/org/onap/cps/temporal/controller/rest/model/AnchorDetailsMapper.java38
-rw-r--r--src/main/java/org/onap/cps/temporal/controller/rest/model/SortMapper.java80
-rw-r--r--src/main/java/org/onap/cps/temporal/controller/utils/DateTimeUtility.java40
-rw-r--r--src/main/java/org/onap/cps/temporal/domain/SearchCriteria.java56
-rw-r--r--src/main/java/org/onap/cps/temporal/service/NetworkDataServiceImpl.java10
-rwxr-xr-xsrc/main/resources/application.yml3
10 files changed, 557 insertions, 17 deletions
diff --git a/src/main/java/org/onap/cps/temporal/controller/event/model/CpsDataUpdatedEventMapper.java b/src/main/java/org/onap/cps/temporal/controller/event/model/CpsDataUpdatedEventMapper.java
index 9ef25d5..d180509 100644
--- a/src/main/java/org/onap/cps/temporal/controller/event/model/CpsDataUpdatedEventMapper.java
+++ b/src/main/java/org/onap/cps/temporal/controller/event/model/CpsDataUpdatedEventMapper.java
@@ -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=========================================================
*/
@@ -21,11 +23,11 @@ package org.onap.cps.temporal.controller.event.model;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.OffsetDateTime;
-import java.time.format.DateTimeFormatter;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.onap.cps.event.model.CpsDataUpdatedEvent;
import org.onap.cps.event.model.Data;
+import org.onap.cps.temporal.controller.utils.DateTimeUtility;
import org.onap.cps.temporal.domain.NetworkData;
/**
@@ -34,8 +36,7 @@ import org.onap.cps.temporal.domain.NetworkData;
@Mapper(componentModel = "spring")
public abstract class CpsDataUpdatedEventMapper {
- private static final DateTimeFormatter ISO_TIMESTAMP_FORMATTER =
- DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
+ private ObjectMapper objectMapper = new ObjectMapper();
@Mapping(source = "content.observedTimestamp", target = "observedTimestamp")
@Mapping(source = "content.dataspaceName", target = "dataspace")
@@ -46,11 +47,11 @@ public abstract class CpsDataUpdatedEventMapper {
public abstract NetworkData eventToEntity(CpsDataUpdatedEvent cpsDataUpdatedEvent);
String map(final Data data) throws JsonProcessingException {
- return data != null ? new ObjectMapper().writeValueAsString(data) : null;
+ return data != null ? objectMapper.writeValueAsString(data) : null;
}
OffsetDateTime map(final String timestamp) {
- return timestamp != null ? OffsetDateTime.parse(timestamp, ISO_TIMESTAMP_FORMATTER) : null;
+ return DateTimeUtility.toOffsetDateTime(timestamp);
}
}
diff --git a/src/main/java/org/onap/cps/temporal/controller/rest/QueryController.java b/src/main/java/org/onap/cps/temporal/controller/rest/QueryController.java
index e7171a0..ab29e19 100644
--- a/src/main/java/org/onap/cps/temporal/controller/rest/QueryController.java
+++ b/src/main/java/org/onap/cps/temporal/controller/rest/QueryController.java
@@ -20,12 +20,19 @@
package org.onap.cps.temporal.controller.rest;
+import java.time.OffsetDateTime;
import javax.validation.Valid;
-import javax.validation.constraints.Max;
+import javax.validation.ValidationException;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
+import org.apache.commons.lang3.StringUtils;
import org.onap.cps.temporal.controller.rest.model.AnchorHistory;
-import org.springframework.http.HttpStatus;
+import org.onap.cps.temporal.controller.rest.model.SortMapper;
+import org.onap.cps.temporal.controller.utils.DateTimeUtility;
+import org.onap.cps.temporal.domain.NetworkData;
+import org.onap.cps.temporal.domain.SearchCriteria;
+import org.onap.cps.temporal.service.NetworkDataService;
+import org.springframework.data.domain.Slice;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -34,19 +41,89 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("${rest.api.base-path}")
public class QueryController implements CpsTemporalQueryApi {
+ private NetworkDataService networkDataService;
+ private SortMapper sortMapper;
+ private QueryResponseFactory queryResponseFactory;
+
+ /**
+ * Constructor.
+ *
+ * @param networkDataService networkDataService
+ * @param sortMapper sortMapper
+ * @param queryResponseFactory anchorHistoryResponseFactory
+ */
+ public QueryController(final NetworkDataService networkDataService,
+ final SortMapper sortMapper,
+ final QueryResponseFactory queryResponseFactory) {
+ this.networkDataService = networkDataService;
+ this.sortMapper = sortMapper;
+ this.queryResponseFactory = queryResponseFactory;
+ }
+
@Override
public ResponseEntity<AnchorHistory> getAnchorDataByName(final String dataspaceName,
- final String anchorName, final @Valid String after, final @Valid String simplePayloadFilter,
+ final String anchorName, final @Valid String observedTimestampAfter,
+ final @Valid String simplePayloadFilter,
final @Valid String pointInTime, final @Min(0) @Valid Integer pageNumber,
- final @Min(0) @Max(10000) @Valid Integer pageLimit, final @Valid String sort) {
- return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
+ final @Min(0) @Valid Integer pageLimit, final @Valid String sortAsString) {
+
+ final var searchCriteriaBuilder =
+ getSearchCriteriaBuilder(observedTimestampAfter, simplePayloadFilter, pointInTime,
+ pageNumber, pageLimit, sortAsString)
+ .dataspaceName(dataspaceName).anchorName(anchorName);
+ final var searchCriteria = searchCriteriaBuilder.build();
+ final Slice<NetworkData> searchResult = networkDataService.searchNetworkData(searchCriteria);
+ final var anchorHistory = queryResponseFactory
+ .createAnchorDataByNameResponse(searchCriteria, searchResult);
+ return ResponseEntity.ok(anchorHistory);
}
@Override
public ResponseEntity<AnchorHistory> getAnchorsDataByFilter(final String dataspaceName,
- final @NotNull @Valid String schemaSetName, final @Valid String after, final @Valid String simplePayloadFilter,
+ final @NotNull @Valid String schemaSetName, final @Valid String observedTimestampAfter,
+ final @Valid String simplePayloadFilter,
final @Valid String pointInTime, final @Min(0) @Valid Integer pageNumber,
- final @Min(0) @Max(10000) @Valid Integer pageLimit, final @Valid String sort) {
- return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
+ final @Min(0) @Valid Integer pageLimit, final @Valid String sortAsString) {
+ final var searchCriteriaBuilder =
+ getSearchCriteriaBuilder(observedTimestampAfter,
+ simplePayloadFilter,
+ pointInTime, pageNumber,
+ pageLimit, sortAsString)
+ .dataspaceName(dataspaceName).schemaSetName(schemaSetName);
+ final var searchCriteria = searchCriteriaBuilder.build();
+ final Slice<NetworkData> searchResult = networkDataService.searchNetworkData(searchCriteria);
+ final var anchorHistory = queryResponseFactory
+ .createAnchorsDataByFilterResponse(searchCriteria, searchResult);
+ return ResponseEntity.ok(anchorHistory);
+ }
+
+ private SearchCriteria.Builder getSearchCriteriaBuilder(final String observedTimestampAfter,
+ final String simplePayloadFilter,
+ final String pointInTime, final Integer pageNumber,
+ final Integer pageLimit, final String sortAsString) {
+
+ final var searchCriteriaBuilder = SearchCriteria.builder()
+ .pagination(pageNumber, pageLimit)
+ .observedAfter(getOffsetDateTime(observedTimestampAfter, "observedTimestampAfter"))
+ .simplePayloadFilter(simplePayloadFilter)
+ .sort(sortMapper.toSort(sortAsString));
+
+ if (!StringUtils.isEmpty(pointInTime)) {
+ searchCriteriaBuilder.createdBefore(getOffsetDateTime(pointInTime, "pointInTime"));
+ }
+
+ return searchCriteriaBuilder;
+
}
+
+ private OffsetDateTime getOffsetDateTime(final String datetime, final String propertyName) {
+ try {
+ return DateTimeUtility.toOffsetDateTime(datetime);
+ } catch (final Exception exception) {
+ throw new ValidationException(
+ String.format("%s must be in '%s' format", propertyName, DateTimeUtility.ISO_TIMESTAMP_PATTERN));
+ }
+ }
+
+
}
diff --git a/src/main/java/org/onap/cps/temporal/controller/rest/QueryExceptionHandler.java b/src/main/java/org/onap/cps/temporal/controller/rest/QueryExceptionHandler.java
new file mode 100644
index 0000000..d620dbe
--- /dev/null
+++ b/src/main/java/org/onap/cps/temporal/controller/rest/QueryExceptionHandler.java
@@ -0,0 +1,73 @@
+/*
+ * ============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.controller.rest;
+
+import javax.validation.ValidationException;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.temporal.controller.rest.model.ErrorMessage;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@RestControllerAdvice(basePackageClasses = QueryController.class)
+public class QueryExceptionHandler {
+
+ @ExceptionHandler({ValidationException.class})
+ public ResponseEntity<ErrorMessage> handleClientError(final ValidationException validationException) {
+ return buildErrorMessage(HttpStatus.BAD_REQUEST, validationException);
+ }
+
+ @ExceptionHandler({IllegalArgumentException.class})
+ public ResponseEntity<ErrorMessage> handleClientError(final IllegalArgumentException illegalArgumentException) {
+ return logAndBuildErrorMessage(HttpStatus.BAD_REQUEST, illegalArgumentException);
+ }
+
+ @ExceptionHandler
+ public ResponseEntity<ErrorMessage> handleInternalServerError(final Exception exception) {
+ return logAndBuildErrorMessage(HttpStatus.INTERNAL_SERVER_ERROR, exception);
+ }
+
+ private ResponseEntity<ErrorMessage> logAndBuildErrorMessage(final HttpStatus httpStatus,
+ final Exception exception) {
+ logException(exception);
+ return buildErrorMessage(httpStatus, exception);
+ }
+
+ private void logException(final Exception exception) {
+ final var message = String.format("Failed to process : %s. Error cause is %s",
+ exception.getMessage(),
+ exception.getCause() != null ? exception.getCause().toString() : null);
+ log.error(message, exception);
+
+ }
+
+ private ResponseEntity<ErrorMessage> buildErrorMessage(final HttpStatus httpStatus,
+ final Exception exception) {
+ final var errorMessage = new ErrorMessage();
+ errorMessage.setStatus(Integer.toString(httpStatus.value()));
+ errorMessage.setMessage(exception.getMessage());
+ return ResponseEntity.status(httpStatus).body(errorMessage);
+ }
+
+
+}
diff --git a/src/main/java/org/onap/cps/temporal/controller/rest/QueryResponseFactory.java b/src/main/java/org/onap/cps/temporal/controller/rest/QueryResponseFactory.java
new file mode 100644
index 0000000..6ac4759
--- /dev/null
+++ b/src/main/java/org/onap/cps/temporal/controller/rest/QueryResponseFactory.java
@@ -0,0 +1,170 @@
+/*
+ * ============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.controller.rest;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.onap.cps.temporal.controller.rest.model.AnchorDetails;
+import org.onap.cps.temporal.controller.rest.model.AnchorDetailsMapper;
+import org.onap.cps.temporal.controller.rest.model.AnchorHistory;
+import org.onap.cps.temporal.controller.rest.model.SortMapper;
+import org.onap.cps.temporal.controller.utils.DateTimeUtility;
+import org.onap.cps.temporal.domain.NetworkData;
+import org.onap.cps.temporal.domain.SearchCriteria;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.stereotype.Component;
+import org.springframework.web.util.UriComponentsBuilder;
+
+@Component
+public class QueryResponseFactory {
+
+ private SortMapper sortMapper;
+ private String basePath;
+ private AnchorDetailsMapper anchorDetailsMapper;
+
+ /**
+ * Constructor.
+ *
+ * @param sortMapper sortMapper
+ * @param anchorDetailsMapper anchorDetailsMapper
+ * @param basePath basePath
+ */
+ public QueryResponseFactory(final SortMapper sortMapper,
+ final AnchorDetailsMapper anchorDetailsMapper,
+ @Value("${rest.api.base-path}") final String basePath) {
+ this.sortMapper = sortMapper;
+ this.anchorDetailsMapper = anchorDetailsMapper;
+ this.basePath = basePath;
+ }
+
+ AnchorHistory createAnchorsDataByFilterResponse(final SearchCriteria searchCriteria,
+ final Slice<NetworkData> response) {
+
+ final var anchorHistory = new AnchorHistory();
+ if (response.hasNext()) {
+ anchorHistory.setNextRecordsLink(
+ toRelativeLink(getAbsoluteLinkForGetAnchorsDataByFilter(searchCriteria, response.nextPageable())));
+ }
+ if (response.hasPrevious()) {
+ anchorHistory.setPreviousRecordsLink(
+ toRelativeLink(
+ getAbsoluteLinkForGetAnchorsDataByFilter(searchCriteria, response.previousPageable())));
+ }
+ anchorHistory.setRecords(convertToAnchorDetails(response.getContent()));
+ return anchorHistory;
+ }
+
+ AnchorHistory createAnchorDataByNameResponse(final SearchCriteria searchCriteria,
+ final Slice<NetworkData> response) {
+
+ final var anchorHistory = new AnchorHistory();
+ if (response.hasNext()) {
+ anchorHistory.setNextRecordsLink(toRelativeLink(
+ getAbsoluteLinkForGetAnchorDataByName(searchCriteria, response.nextPageable())));
+ }
+ if (response.hasPrevious()) {
+ anchorHistory.setPreviousRecordsLink(toRelativeLink(
+ getAbsoluteLinkForGetAnchorDataByName(searchCriteria, response.previousPageable())));
+ }
+ anchorHistory.setRecords(convertToAnchorDetails(response.getContent()));
+ return anchorHistory;
+ }
+
+ private List<AnchorDetails> convertToAnchorDetails(final List<NetworkData> networkDataList) {
+ return networkDataList.stream()
+ .map(networkData -> anchorDetailsMapper.toAnchorDetails(networkData))
+ .collect(Collectors.toList());
+ }
+
+ /*
+ Spring hateoas only provides absolute link. But in the microservices, relative links will be more appropriate
+ */
+ private String toRelativeLink(final String absoluteLink) {
+
+ /* Spring hateoas Issue:
+ It does replace the variable defined at the Controller level,
+ so we are removing the variable name and replace it with basePath.
+ https://github.com/spring-projects/spring-hateoas/issues/361
+ https://github.com/spring-projects/spring-hateoas/pull/1375
+ */
+ final int contextPathBeginIndex = absoluteLink.indexOf("rest.api.base-path%257D");
+ return basePath + absoluteLink.substring(contextPathBeginIndex + 23);
+ }
+
+ private String getAbsoluteLinkForGetAnchorDataByName(final SearchCriteria searchCriteria,
+ final Pageable pageable) {
+ final var uriComponentsBuilder = linkTo(methodOn(QueryController.class).getAnchorDataByName(
+ searchCriteria.getDataspaceName(),
+ searchCriteria.getAnchorName(),
+ DateTimeUtility.toString(searchCriteria.getObservedAfter()),
+ null,
+ DateTimeUtility.toString(searchCriteria.getCreatedBefore()),
+ pageable.getPageNumber(), pageable.getPageSize(),
+ sortMapper.sortAsString(searchCriteria.getPageable().getSort())))
+ .toUriComponentsBuilder();
+ addSimplePayloadFilter(uriComponentsBuilder, searchCriteria.getSimplePayloadFilter());
+ return encodePlusSign(uriComponentsBuilder.toUriString());
+ }
+
+ private String getAbsoluteLinkForGetAnchorsDataByFilter(final SearchCriteria searchCriteria,
+ final Pageable pageable) {
+ final var uriComponentsBuilder = linkTo(methodOn(QueryController.class).getAnchorsDataByFilter(
+ searchCriteria.getDataspaceName(),
+ searchCriteria.getSchemaSetName(),
+ DateTimeUtility.toString(searchCriteria.getObservedAfter()),
+ null,
+ DateTimeUtility.toString(searchCriteria.getCreatedBefore()),
+ pageable.getPageNumber(), pageable.getPageSize(),
+ sortMapper.sortAsString(searchCriteria.getPageable().getSort())))
+ .toUriComponentsBuilder();
+ addSimplePayloadFilter(uriComponentsBuilder, searchCriteria.getSimplePayloadFilter());
+ return encodePlusSign(uriComponentsBuilder.toUriString());
+ }
+
+ /*
+ Spring hateoas does double encoding when generting URI.
+ To avoid it in the case of simplePayloadFilter,
+ the 'simplePayloadFilter is being added explicitly to UriComponentsBuilder
+ */
+ private UriComponentsBuilder addSimplePayloadFilter(final UriComponentsBuilder uriComponentsBuilder,
+ final String simplePayloadFilter) {
+ if (simplePayloadFilter != null) {
+ uriComponentsBuilder.queryParam("simplePayloadFilter", simplePayloadFilter);
+ }
+ return uriComponentsBuilder;
+ }
+
+ /*
+ Spring hateoas does not encode '+' in the query param but it deccodes '+' as space.
+ Due to this inconsistency, API was failing to convert datetime with positive timezone.
+ The fix is done in the spring-hateoas 1.4 version but it is yet to release.
+ As a workaround, we are replacing all the '+' with '%2B'
+ https://github.com/spring-projects/spring-hateoas/issues/1485
+ */
+ private String encodePlusSign(final String link) {
+ return link.replace("+", "%2B");
+ }
+}
diff --git a/src/main/java/org/onap/cps/temporal/controller/rest/model/AnchorDetailsMapper.java b/src/main/java/org/onap/cps/temporal/controller/rest/model/AnchorDetailsMapper.java
new file mode 100644
index 0000000..1c44c36
--- /dev/null
+++ b/src/main/java/org/onap/cps/temporal/controller/rest/model/AnchorDetailsMapper.java
@@ -0,0 +1,38 @@
+/*
+ * ============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.controller.rest.model;
+
+import java.time.OffsetDateTime;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.onap.cps.temporal.controller.utils.DateTimeUtility;
+import org.onap.cps.temporal.domain.NetworkData;
+
+@Mapper(componentModel = "spring")
+public interface AnchorDetailsMapper {
+
+ @Mapping(source = "payload", target = "data")
+ AnchorDetails toAnchorDetails(NetworkData networkData);
+
+ default String map(final OffsetDateTime timestamp) {
+ return DateTimeUtility.toString(timestamp);
+ }
+}
diff --git a/src/main/java/org/onap/cps/temporal/controller/rest/model/SortMapper.java b/src/main/java/org/onap/cps/temporal/controller/rest/model/SortMapper.java
new file mode 100644
index 0000000..cd553eb
--- /dev/null
+++ b/src/main/java/org/onap/cps/temporal/controller/rest/model/SortMapper.java
@@ -0,0 +1,80 @@
+/*
+ * ============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.controller.rest.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import javax.validation.ValidationException;
+import javax.validation.constraints.NotEmpty;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Direction;
+import org.springframework.data.domain.Sort.Order;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SortMapper {
+
+ private static final String SORT_ORDER_SEPARATOR = ",";
+ private static final String FIELD_DIRECTION_SEPARATOR = ":";
+
+ /**
+ * convert from Sort to String format "fieldname:direction,...,fieldname:direction".
+ *
+ * @param sort sort
+ * @return sort string
+ */
+ public String sortAsString(final Sort sort) {
+ return sort.stream()
+ .map(sortOrder ->
+ sortOrder.getProperty() + FIELD_DIRECTION_SEPARATOR
+ + sortOrder.getDirection().toString().toLowerCase(Locale.ENGLISH))
+ .collect(Collectors.joining(SORT_ORDER_SEPARATOR));
+ }
+
+ /**
+ * Convert from "fieldname:direction,...,fieldname:direction" format to Sort. Example :
+ * "anchor:asc,observed_timestamp:desc"
+ *
+ * @param sortString sortString
+ * @return Sort
+ */
+ public Sort toSort(@NotEmpty final String sortString) {
+ try {
+ final String[] sortingOrderAsString = sortString.split(SORT_ORDER_SEPARATOR);
+ final List<Order> sortOrder = new ArrayList<>();
+ for (final String eachSortAsString : sortingOrderAsString) {
+ final String[] eachSortDetail = eachSortAsString.split(FIELD_DIRECTION_SEPARATOR);
+ final var direction = Direction.fromString(eachSortDetail[1]);
+ final var fieldName = eachSortDetail[0];
+ sortOrder.add(new Order(direction, fieldName));
+ }
+ return Sort.by(sortOrder);
+ } catch (final Exception exception) {
+ throw new ValidationException(
+ String.format(
+ "Invalid sort format. sort '%s' is not in '<fieldname>:<direction>,...,<fieldname>:<direction>'"
+ + " format. Example: 'anchor:asc,observed_timestamp:desc'", sortString), exception
+ );
+ }
+ }
+}
diff --git a/src/main/java/org/onap/cps/temporal/controller/utils/DateTimeUtility.java b/src/main/java/org/onap/cps/temporal/controller/utils/DateTimeUtility.java
new file mode 100644
index 0000000..b36904e
--- /dev/null
+++ b/src/main/java/org/onap/cps/temporal/controller/utils/DateTimeUtility.java
@@ -0,0 +1,40 @@
+/*
+ * ============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.controller.utils;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import org.apache.commons.lang3.StringUtils;
+
+public interface DateTimeUtility {
+
+ String ISO_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
+ DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_PATTERN);
+
+ static OffsetDateTime toOffsetDateTime(String datetTimestampAsString) {
+ return StringUtils.isEmpty(datetTimestampAsString)
+ ? null : OffsetDateTime.parse(datetTimestampAsString, ISO_TIMESTAMP_FORMATTER);
+ }
+
+ static String toString(OffsetDateTime offsetDateTime) {
+ return offsetDateTime != null ? ISO_TIMESTAMP_FORMATTER.format(offsetDateTime) : null;
+ }
+}
diff --git a/src/main/java/org/onap/cps/temporal/domain/SearchCriteria.java b/src/main/java/org/onap/cps/temporal/domain/SearchCriteria.java
index 8188d84..4cd6a20 100644
--- a/src/main/java/org/onap/cps/temporal/domain/SearchCriteria.java
+++ b/src/main/java/org/onap/cps/temporal/domain/SearchCriteria.java
@@ -20,10 +20,14 @@
package org.onap.cps.temporal.domain;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import java.time.OffsetDateTime;
-import javax.validation.constraints.NotNull;
+import java.util.List;
import lombok.AccessLevel;
import lombok.Builder;
+import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
@@ -31,10 +35,12 @@ 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;
+import org.springframework.data.domain.Sort.Order;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(builderClassName = "Builder")
+@EqualsAndHashCode
public class SearchCriteria {
private OffsetDateTime createdBefore;
@@ -47,7 +53,14 @@ public class SearchCriteria {
public static class Builder {
- private Sort sort = Sort.by(Direction.DESC, "observed_timestamp");
+ private static final String OBSERVED_TIMESTAMP_FIELD_NAME = "observed_timestamp";
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private static final List<Order> REQUIRED_SORT_ORDERS = List.of(Order.desc(OBSERVED_TIMESTAMP_FIELD_NAME));
+ private static final List<Order> SUPPORTED_SORT_ORDERS = List.of(Order.desc(OBSERVED_TIMESTAMP_FIELD_NAME),
+ Order.asc("anchor"));
+
+ private Sort sort = Sort.by(Direction.DESC, OBSERVED_TIMESTAMP_FIELD_NAME);
private OffsetDateTime createdBefore = OffsetDateTime.now();
public Builder pagination(final int pageNumber, final int pageSize) {
@@ -55,7 +68,44 @@ public class SearchCriteria {
return this;
}
- public Builder sort(final @NotNull Sort sort) {
+ /**
+ * Validate that simplePayloadFilter is a valid json.
+ *
+ * @param simplePayloadFilter simplePayloadFilter
+ * @return Builder
+ */
+ public Builder simplePayloadFilter(final String simplePayloadFilter) {
+ if (!StringUtils.isEmpty(simplePayloadFilter)) {
+ try {
+ OBJECT_MAPPER.readValue(simplePayloadFilter, ObjectNode.class);
+ this.simplePayloadFilter = simplePayloadFilter;
+ } catch (final JsonProcessingException jsonProcessingException) {
+ throw new IllegalArgumentException("simplePayloadFilter must be a valid json");
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Validates the input with the expected list and saves only if matches.
+ *
+ * @param sort sort
+ * @return Builder builder
+ */
+ public Builder sort(final Sort sort) {
+ if (sort == null) {
+ throw new IllegalArgumentException("sort must not be null");
+ }
+ final List<Order> sortOrders = sort.toList();
+ if (!SUPPORTED_SORT_ORDERS.containsAll(sortOrders)) {
+ throw new IllegalArgumentException(
+ "Invalid sorting. Supported sorts are " + SUPPORTED_SORT_ORDERS.toString());
+ }
+ if (!sortOrders.containsAll(REQUIRED_SORT_ORDERS)) {
+ throw new IllegalArgumentException(
+ "Missing mandatory sort. Required sorts are " + REQUIRED_SORT_ORDERS.toString());
+ }
+
this.sort = sort;
return this;
}
diff --git a/src/main/java/org/onap/cps/temporal/service/NetworkDataServiceImpl.java b/src/main/java/org/onap/cps/temporal/service/NetworkDataServiceImpl.java
index 7c2f999..3eba6fb 100644
--- a/src/main/java/org/onap/cps/temporal/service/NetworkDataServiceImpl.java
+++ b/src/main/java/org/onap/cps/temporal/service/NetworkDataServiceImpl.java
@@ -21,11 +21,13 @@
package org.onap.cps.temporal.service;
import java.util.Optional;
+import javax.validation.ValidationException;
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.beans.factory.annotation.Value;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
@@ -37,9 +39,12 @@ import org.springframework.stereotype.Service;
public class NetworkDataServiceImpl implements NetworkDataService {
private final NetworkDataRepository networkDataRepository;
+ private final int maxPageSize;
- public NetworkDataServiceImpl(final NetworkDataRepository networkDataRepository) {
+ public NetworkDataServiceImpl(final NetworkDataRepository networkDataRepository,
+ final @Value("${app.query.response.max-page-size}") int maxPageSize) {
this.networkDataRepository = networkDataRepository;
+ this.maxPageSize = maxPageSize;
}
@Override
@@ -59,6 +64,9 @@ public class NetworkDataServiceImpl implements NetworkDataService {
@Override
public Slice<NetworkData> searchNetworkData(final SearchCriteria searchCriteria) {
+ if (searchCriteria.getPageable().getPageSize() > maxPageSize) {
+ throw new ValidationException("page-size must be less than or equals to " + maxPageSize);
+ }
return networkDataRepository.findBySearchCriteria(searchCriteria);
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index c79351a..41eddf8 100755
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -57,6 +57,9 @@ app:
listener:
data-updated:
topic: ${CPS_CHANGE_EVENT_TOPIC:cps.cfg-state-events}
+ query:
+ response:
+ max-page-size: 10000
springdoc:
swagger-ui: