2 * ============LICENSE_START=======================================================
3 * Copyright (c) 2021 Bell Canada.
4 * ================================================================================
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
21 package org.onap.cps.temporal.controller.rest;
23 import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
24 import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
26 import java.time.OffsetDateTime;
27 import java.util.List;
28 import java.util.stream.Collectors;
29 import javax.validation.Valid;
30 import javax.validation.ValidationException;
31 import javax.validation.constraints.Min;
32 import javax.validation.constraints.NotNull;
33 import org.apache.commons.lang3.StringUtils;
34 import org.onap.cps.temporal.controller.rest.model.AnchorDetails;
35 import org.onap.cps.temporal.controller.rest.model.AnchorDetailsMapper;
36 import org.onap.cps.temporal.controller.rest.model.AnchorHistory;
37 import org.onap.cps.temporal.controller.rest.model.SortMapper;
38 import org.onap.cps.temporal.controller.utils.DateTimeUtility;
39 import org.onap.cps.temporal.domain.NetworkData;
40 import org.onap.cps.temporal.domain.SearchCriteria;
41 import org.onap.cps.temporal.service.NetworkDataService;
42 import org.springframework.beans.factory.annotation.Value;
43 import org.springframework.data.domain.Pageable;
44 import org.springframework.data.domain.Slice;
45 import org.springframework.http.ResponseEntity;
46 import org.springframework.web.bind.annotation.RequestMapping;
47 import org.springframework.web.bind.annotation.RestController;
48 import org.springframework.web.util.UriComponentsBuilder;
51 @RequestMapping("${rest.api.base-path}")
52 public class QueryController implements CpsTemporalQueryApi {
54 private NetworkDataService networkDataService;
55 private SortMapper sortMapper;
56 private QueryResponseFactory queryResponseFactory;
61 * @param networkDataService networkDataService
62 * @param sortMapper sortMapper
63 * @param anchorDetailsMapper anchorDetailsMapper
64 * @param basePath basePath
66 public QueryController(final NetworkDataService networkDataService,
67 final SortMapper sortMapper,
68 final AnchorDetailsMapper anchorDetailsMapper,
69 @Value("${rest.api.base-path}") final String basePath) {
70 this.networkDataService = networkDataService;
71 this.sortMapper = sortMapper;
72 this.queryResponseFactory = new QueryResponseFactory(sortMapper, anchorDetailsMapper, basePath);
76 public ResponseEntity<AnchorHistory> getAnchorDataByName(final String dataspaceName,
77 final String anchorName, final @Valid String observedTimestampAfter,
78 final @Valid String simplePayloadFilter,
79 final @Valid String pointInTime, final @Min(0) @Valid Integer pageNumber,
80 final @Min(0) @Valid Integer pageLimit, final @Valid String sortAsString) {
82 final var searchCriteriaBuilder =
83 getSearchCriteriaBuilder(observedTimestampAfter, simplePayloadFilter, pointInTime,
84 pageNumber, pageLimit, sortAsString)
85 .dataspaceName(dataspaceName).anchorName(anchorName);
86 final var searchCriteria = searchCriteriaBuilder.build();
87 final Slice<NetworkData> searchResult = networkDataService.searchNetworkData(searchCriteria);
88 final var anchorHistory = queryResponseFactory
89 .createAnchorDataByNameResponse(searchCriteria, searchResult);
90 return ResponseEntity.ok(anchorHistory);
94 public ResponseEntity<AnchorHistory> getAnchorsDataByFilter(final String dataspaceName,
95 final @NotNull @Valid String schemaSetName, final @Valid String observedTimestampAfter,
96 final @Valid String simplePayloadFilter,
97 final @Valid String pointInTime, final @Min(0) @Valid Integer pageNumber,
98 final @Min(0) @Valid Integer pageLimit, final @Valid String sortAsString) {
99 final var searchCriteriaBuilder =
100 getSearchCriteriaBuilder(observedTimestampAfter,
102 pointInTime, pageNumber,
103 pageLimit, sortAsString)
104 .dataspaceName(dataspaceName).schemaSetName(schemaSetName);
105 final var searchCriteria = searchCriteriaBuilder.build();
106 final Slice<NetworkData> searchResult = networkDataService.searchNetworkData(searchCriteria);
107 final var anchorHistory = queryResponseFactory
108 .createAnchorsDataByFilterResponse(searchCriteria, searchResult);
109 return ResponseEntity.ok(anchorHistory);
112 private SearchCriteria.Builder getSearchCriteriaBuilder(final String observedTimestampAfter,
113 final String simplePayloadFilter,
114 final String pointInTime, final Integer pageNumber,
115 final Integer pageLimit, final String sortAsString) {
117 final var searchCriteriaBuilder = SearchCriteria.builder()
118 .pagination(pageNumber, pageLimit)
119 .observedAfter(getOffsetDateTime(observedTimestampAfter, "observedTimestampAfter"))
120 .simplePayloadFilter(simplePayloadFilter)
121 .sort(sortMapper.toSort(sortAsString));
123 if (!StringUtils.isEmpty(pointInTime)) {
124 searchCriteriaBuilder.createdBefore(getOffsetDateTime(pointInTime, "pointInTime"));
127 return searchCriteriaBuilder;
131 private OffsetDateTime getOffsetDateTime(final String datetime, final String propertyName) {
133 return DateTimeUtility.toOffsetDateTime(datetime);
134 } catch (final Exception exception) {
135 throw new ValidationException(
136 String.format("%s must be in '%s' format", propertyName, DateTimeUtility.ISO_TIMESTAMP_PATTERN));
141 public static class QueryResponseFactory {
143 private SortMapper sortMapper;
144 private String basePath;
145 private AnchorDetailsMapper anchorDetailsMapper;
150 * @param sortMapper sortMapper
151 * @param anchorDetailsMapper anchorDetailsMapper
152 * @param basePath basePath
154 public QueryResponseFactory(final SortMapper sortMapper,
155 final AnchorDetailsMapper anchorDetailsMapper,
156 final String basePath) {
157 this.sortMapper = sortMapper;
158 this.anchorDetailsMapper = anchorDetailsMapper;
159 this.basePath = basePath;
163 * Use search criteria and search result-set to create response.
165 * @param searchCriteria searchCriteria
166 * @param searchResult searchResult
167 * @return AnchorHistory
169 public AnchorHistory createAnchorsDataByFilterResponse(final SearchCriteria searchCriteria,
170 final Slice<NetworkData> searchResult) {
172 final var anchorHistory = new AnchorHistory();
173 if (searchResult.hasNext()) {
174 anchorHistory.setNextRecordsLink(
176 getAbsoluteLinkForGetAnchorsDataByFilter(searchCriteria, searchResult.nextPageable())));
178 if (searchResult.hasPrevious()) {
179 anchorHistory.setPreviousRecordsLink(
181 getAbsoluteLinkForGetAnchorsDataByFilter(searchCriteria, searchResult.previousPageable())));
183 anchorHistory.setRecords(convertToAnchorDetails(searchResult.getContent()));
184 return anchorHistory;
188 * Use search criteria and search result-set to create response.
190 * @param searchCriteria searchCriteria
191 * @param searchResult searchResult
192 * @return AnchorHistory
194 public AnchorHistory createAnchorDataByNameResponse(final SearchCriteria searchCriteria,
195 final Slice<NetworkData> searchResult) {
197 final var anchorHistory = new AnchorHistory();
198 if (searchResult.hasNext()) {
199 anchorHistory.setNextRecordsLink(toRelativeLink(
200 getAbsoluteLinkForGetAnchorDataByName(searchCriteria, searchResult.nextPageable())));
202 if (searchResult.hasPrevious()) {
203 anchorHistory.setPreviousRecordsLink(toRelativeLink(
204 getAbsoluteLinkForGetAnchorDataByName(searchCriteria, searchResult.previousPageable())));
206 anchorHistory.setRecords(convertToAnchorDetails(searchResult.getContent()));
207 return anchorHistory;
210 private List<AnchorDetails> convertToAnchorDetails(final List<NetworkData> networkDataList) {
211 return networkDataList.stream()
212 .map(networkData -> anchorDetailsMapper.toAnchorDetails(networkData))
213 .collect(Collectors.toList());
217 Spring hateoas only provides absolute link. But in the microservices, relative links will be more appropriate
219 private String toRelativeLink(final String absoluteLink) {
221 /* Spring hateoas Issue:
222 It does replace the variable defined at the Controller level,
223 so we are removing the variable name and replace it with basePath.
224 https://github.com/spring-projects/spring-hateoas/issues/361
225 https://github.com/spring-projects/spring-hateoas/pull/1375
227 final int contextPathBeginIndex = absoluteLink.indexOf("rest.api.base-path%257D");
228 return basePath + absoluteLink.substring(contextPathBeginIndex + 23);
231 private String getAbsoluteLinkForGetAnchorDataByName(final SearchCriteria searchCriteria,
232 final Pageable pageable) {
233 final var uriComponentsBuilder = linkTo(methodOn(QueryController.class).getAnchorDataByName(
234 searchCriteria.getDataspaceName(),
235 searchCriteria.getAnchorName(),
236 DateTimeUtility.toString(searchCriteria.getObservedAfter()),
238 DateTimeUtility.toString(searchCriteria.getCreatedBefore()),
239 pageable.getPageNumber(), pageable.getPageSize(),
240 sortMapper.sortAsString(searchCriteria.getPageable().getSort())))
241 .toUriComponentsBuilder();
242 addSimplePayloadFilter(uriComponentsBuilder, searchCriteria.getSimplePayloadFilter());
243 return encodePlusSign(uriComponentsBuilder.toUriString());
246 private String getAbsoluteLinkForGetAnchorsDataByFilter(final SearchCriteria searchCriteria,
247 final Pageable pageable) {
248 final var uriComponentsBuilder = linkTo(methodOn(QueryController.class).getAnchorsDataByFilter(
249 searchCriteria.getDataspaceName(),
250 searchCriteria.getSchemaSetName(),
251 DateTimeUtility.toString(searchCriteria.getObservedAfter()),
253 DateTimeUtility.toString(searchCriteria.getCreatedBefore()),
254 pageable.getPageNumber(), pageable.getPageSize(),
255 sortMapper.sortAsString(searchCriteria.getPageable().getSort())))
256 .toUriComponentsBuilder();
257 addSimplePayloadFilter(uriComponentsBuilder, searchCriteria.getSimplePayloadFilter());
258 return encodePlusSign(uriComponentsBuilder.toUriString());
262 Spring hateoas does double encoding when generting URI.
263 To avoid it in the case of simplePayloadFilter,
264 the 'simplePayloadFilter is being added explicitly to UriComponentsBuilder
266 private UriComponentsBuilder addSimplePayloadFilter(final UriComponentsBuilder uriComponentsBuilder,
267 final String simplePayloadFilter) {
268 if (simplePayloadFilter != null) {
269 uriComponentsBuilder.queryParam("simplePayloadFilter", simplePayloadFilter);
271 return uriComponentsBuilder;
275 Spring hateoas does not encode '+' in the query param but it deccodes '+' as space.
276 Due to this inconsistency, API was failing to convert datetime with positive timezone.
277 The fix is done in the spring-hateoas 1.4 version but it is yet to release.
278 As a workaround, we are replacing all the '+' with '%2B'
279 https://github.com/spring-projects/spring-hateoas/issues/1485
281 private String encodePlusSign(final String link) {
282 return link.replace("+", "%2B");