Add basic security to query interface
[cps/cps-temporal.git] / src / main / java / org / onap / cps / temporal / controller / rest / QueryController.java
1 /*
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
8  *
9  *         http://www.apache.org/licenses/LICENSE-2.0
10  *
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.
16  *
17  * SPDX-License-Identifier: Apache-2.0
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.cps.temporal.controller.rest;
22
23 import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
24 import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
25
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;
49
50 @RestController
51 @RequestMapping("${rest.api.base-path}")
52 public class QueryController implements CpsTemporalQueryApi {
53
54     private NetworkDataService networkDataService;
55     private SortMapper sortMapper;
56     private QueryResponseFactory queryResponseFactory;
57
58     /**
59      * Constructor.
60      *
61      * @param networkDataService  networkDataService
62      * @param sortMapper          sortMapper
63      * @param anchorDetailsMapper anchorDetailsMapper
64      * @param basePath            basePath
65      */
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);
73     }
74
75     @Override
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) {
81
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);
91     }
92
93     @Override
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,
101                 simplePayloadFilter,
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);
110     }
111
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) {
116
117         final var searchCriteriaBuilder = SearchCriteria.builder()
118             .pagination(pageNumber, pageLimit)
119             .observedAfter(getOffsetDateTime(observedTimestampAfter, "observedTimestampAfter"))
120             .simplePayloadFilter(simplePayloadFilter)
121             .sort(sortMapper.toSort(sortAsString));
122
123         if (!StringUtils.isEmpty(pointInTime)) {
124             searchCriteriaBuilder.createdBefore(getOffsetDateTime(pointInTime, "pointInTime"));
125         }
126
127         return searchCriteriaBuilder;
128
129     }
130
131     private OffsetDateTime getOffsetDateTime(final String datetime, final String propertyName) {
132         try {
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));
137         }
138     }
139
140
141     public static class QueryResponseFactory {
142
143         private SortMapper sortMapper;
144         private String basePath;
145         private AnchorDetailsMapper anchorDetailsMapper;
146
147         /**
148          * Constructor.
149          *
150          * @param sortMapper          sortMapper
151          * @param anchorDetailsMapper anchorDetailsMapper
152          * @param basePath            basePath
153          */
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;
160         }
161
162         /**
163          * Use search criteria and search result-set to create response.
164          *
165          * @param searchCriteria searchCriteria
166          * @param searchResult   searchResult
167          * @return AnchorHistory
168          */
169         public AnchorHistory createAnchorsDataByFilterResponse(final SearchCriteria searchCriteria,
170             final Slice<NetworkData> searchResult) {
171
172             final var anchorHistory = new AnchorHistory();
173             if (searchResult.hasNext()) {
174                 anchorHistory.setNextRecordsLink(
175                     toRelativeLink(
176                         getAbsoluteLinkForGetAnchorsDataByFilter(searchCriteria, searchResult.nextPageable())));
177             }
178             if (searchResult.hasPrevious()) {
179                 anchorHistory.setPreviousRecordsLink(
180                     toRelativeLink(
181                         getAbsoluteLinkForGetAnchorsDataByFilter(searchCriteria, searchResult.previousPageable())));
182             }
183             anchorHistory.setRecords(convertToAnchorDetails(searchResult.getContent()));
184             return anchorHistory;
185         }
186
187         /**
188          * Use search criteria and search result-set to create response.
189          *
190          * @param searchCriteria searchCriteria
191          * @param searchResult   searchResult
192          * @return AnchorHistory
193          */
194         public AnchorHistory createAnchorDataByNameResponse(final SearchCriteria searchCriteria,
195             final Slice<NetworkData> searchResult) {
196
197             final var anchorHistory = new AnchorHistory();
198             if (searchResult.hasNext()) {
199                 anchorHistory.setNextRecordsLink(toRelativeLink(
200                     getAbsoluteLinkForGetAnchorDataByName(searchCriteria, searchResult.nextPageable())));
201             }
202             if (searchResult.hasPrevious()) {
203                 anchorHistory.setPreviousRecordsLink(toRelativeLink(
204                     getAbsoluteLinkForGetAnchorDataByName(searchCriteria, searchResult.previousPageable())));
205             }
206             anchorHistory.setRecords(convertToAnchorDetails(searchResult.getContent()));
207             return anchorHistory;
208         }
209
210         private List<AnchorDetails> convertToAnchorDetails(final List<NetworkData> networkDataList) {
211             return networkDataList.stream()
212                 .map(networkData -> anchorDetailsMapper.toAnchorDetails(networkData))
213                 .collect(Collectors.toList());
214         }
215
216         /*
217         Spring hateoas only provides absolute link. But in the microservices, relative links will be more appropriate
218          */
219         private String toRelativeLink(final String absoluteLink) {
220
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
226              */
227             final int contextPathBeginIndex = absoluteLink.indexOf("rest.api.base-path%257D");
228             return basePath + absoluteLink.substring(contextPathBeginIndex + 23);
229         }
230
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()),
237                 null,
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());
244         }
245
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()),
252                 null,
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());
259         }
260
261         /*
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
265          */
266         private UriComponentsBuilder addSimplePayloadFilter(final UriComponentsBuilder uriComponentsBuilder,
267             final String simplePayloadFilter) {
268             if (simplePayloadFilter != null) {
269                 uriComponentsBuilder.queryParam("simplePayloadFilter", simplePayloadFilter);
270             }
271             return uriComponentsBuilder;
272         }
273
274         /*
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
280          */
281         private String encodePlusSign(final String link) {
282             return link.replace("+", "%2B");
283         }
284     }
285 }