007c666ac25b0acab804ee5e85e598850108238b
[so.git] / common / src / main / java / org / onap / so / client / RestClient.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP - SO
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  * 
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  * 
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.so.client;
22
23 import java.net.ConnectException;
24 import java.net.MalformedURLException;
25 import java.net.SocketTimeoutException;
26 import java.net.URI;
27 import java.net.URL;
28 import java.security.GeneralSecurityException;
29 import java.util.ArrayList;
30 import java.util.Base64;
31 import java.util.Collections;
32 import java.util.HashMap;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Map.Entry;
36 import java.util.Optional;
37 import java.util.concurrent.TimeUnit;
38 import java.util.function.Predicate;
39 import javax.ws.rs.client.Client;
40 import javax.ws.rs.client.ClientBuilder;
41 import javax.ws.rs.client.Invocation.Builder;
42 import javax.ws.rs.client.WebTarget;
43 import javax.ws.rs.core.GenericType;
44 import javax.ws.rs.core.MediaType;
45 import javax.ws.rs.core.Response;
46 import javax.ws.rs.core.Response.Status;
47 import javax.ws.rs.core.UriBuilder;
48 import org.onap.so.client.policy.CommonObjectMapperProvider;
49 import org.onap.so.logging.jaxrs.filter.JaxRsClientLogging;
50 import org.onap.so.logging.jaxrs.filter.PayloadLoggingFilter;
51 import org.onap.so.utils.CryptoUtils;
52 import org.onap.so.utils.TargetEntity;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55 import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
56 import net.jodah.failsafe.Failsafe;
57 import net.jodah.failsafe.RetryPolicy;
58
59
60 public abstract class RestClient {
61         private static final String APPLICATION_MERGE_PATCH_JSON = "application/merge-patch+json";
62
63     public static final String ECOMP_COMPONENT_NAME = "MSO";
64         
65         private static final int MAX_PAYLOAD_SIZE = 1024 * 1024;
66         private WebTarget webTarget;
67
68         protected final Map<String, String> headerMap;
69         protected final Logger logger = LoggerFactory.getLogger(RestClient.class);
70         protected URL host;
71         protected Optional<URI> path;
72         protected String accept;
73         protected String contentType;
74         protected String requestId;
75     protected JaxRsClientLogging jaxRsClientLogging;
76     protected RestProperties props;
77
78     protected RestClient(RestProperties props, Optional<URI> path) {
79                 
80                 headerMap = new HashMap<>();
81                 try {
82                         host = props.getEndpoint();
83                 } catch (MalformedURLException e) {
84                         
85                         throw new RuntimeException(e);
86                 }
87                 this.props = props;
88                 this.path = path;
89         }
90
91         protected RestClient(RestProperties props, Optional<URI> path, String accept, String contentType) {
92                 this(props, path);
93                 this.accept = accept;
94                 this.contentType = contentType;
95                 this.props = props;
96         }
97
98         protected RestClient(URL host, String contentType) {
99                 headerMap = new HashMap<>();
100                 this.path = Optional.empty();
101                 this.host = host;
102                 this.contentType = contentType;
103                 this.props = new DefaultProperties(host);
104         }
105
106         /**
107          * Override method to return false to disable logging.
108          * 
109          * @return true - to enable logging, false otherwise
110          */
111         protected boolean enableLogging() {
112                 return true;
113         }
114         
115         /**
116          * Override method to return custom value for max payload size.
117          * 
118          * @return Default value for MAX_PAYLOAD_SIZE = 1024 * 1024
119          */
120         protected int getMaxPayloadSize()
121         {
122                 return MAX_PAYLOAD_SIZE;
123         }
124         
125         protected Builder getBuilder() {
126
127             if (webTarget == null) {
128                 initializeClient(getClient());
129             }
130             Builder builder = webTarget.request();
131             initializeHeaderMap(headerMap);
132             for (Entry<String, String> entry : headerMap.entrySet()) {
133                 builder.header(entry.getKey(), entry.getValue());
134             }
135             return builder;
136         }
137         
138         protected WebTarget getWebTarget() {
139                 return this.webTarget;
140         }
141
142         protected abstract void initializeHeaderMap(Map<String, String> headerMap);
143
144         protected Optional<ResponseExceptionMapper> addResponseExceptionMapper() {
145                 return Optional.of(new ResponseExceptionMapperImpl());
146         }
147
148         protected CommonObjectMapperProvider getCommonObjectMapperProvider() {
149                 return new CommonObjectMapperProvider();
150         }
151         
152         /**
153          * Adds a basic authentication header to the request.
154          * @param auth the encrypted credentials
155          * @param key the key for decrypting the credentials
156          */
157         protected void addBasicAuthHeader(String auth, String key) {
158                 try {
159                         byte[] decryptedAuth = CryptoUtils.decrypt(auth, key).getBytes();
160                         String authHeaderValue = "Basic " + Base64.getEncoder().encodeToString(decryptedAuth);
161                         headerMap.put("Authorization", authHeaderValue);
162                 } catch (GeneralSecurityException e) {
163                         logger.error(e.getMessage(), e);
164                 }
165         }
166
167         protected String getAccept() {
168                 return accept;
169         }
170
171         protected String getContentType() {
172                 return contentType;
173         }
174
175         protected String getMergeContentType() {
176                 return APPLICATION_MERGE_PATCH_JSON;
177         }
178
179         protected Client getClient() {
180                 return ClientBuilder.newBuilder().build();
181         }
182
183         protected abstract TargetEntity getTargetEntity();
184
185         protected void initializeClient(Client client) {
186                 if (this.enableLogging()) {
187                         client.register(new PayloadLoggingFilter(this.getMaxPayloadSize()));
188                 }
189                 CommonObjectMapperProvider provider = this.getCommonObjectMapperProvider();
190                 client.register(new JacksonJsonProvider(provider.getMapper()));
191                 
192         jaxRsClientLogging = new JaxRsClientLogging();
193         jaxRsClientLogging.setTargetService(getTargetEntity());
194         client.register(jaxRsClientLogging);
195
196         if (!path.isPresent()) {
197                         webTarget = client.target(host.toString());
198                 } else {
199                         webTarget = client.target(UriBuilder.fromUri(host + path.get().toString()));
200                 }
201                 if (getAccept() == null || getAccept().isEmpty()) {
202                         this.accept = MediaType.APPLICATION_JSON;
203                 }
204                 if (getContentType() == null || getContentType().isEmpty()) {
205                         this.contentType = MediaType.APPLICATION_JSON;
206                 }
207         }
208         
209         protected List<Predicate<Throwable>> retryOn() {
210                 
211                 List<Predicate<Throwable>> result = new ArrayList<>();
212                 
213                 result.add(e -> {
214                                         return e.getCause() instanceof SocketTimeoutException;
215                                 });
216                 result.add(e -> {
217                         return e.getCause() instanceof ConnectException;
218                 });
219                 return result;
220         }
221
222         public Response get() {
223                 return method("GET", null);
224         }
225
226         public Response post(Object obj) {
227                 return method("POST", obj);
228         }
229
230         public Response patch(Object obj) {
231                 return method("PATCH", obj);
232         }
233
234         public Response put(Object obj) {
235                 return method("PUT", obj);
236         }
237
238         public Response delete() {
239                 return method("DELETE", null);
240         }
241
242         public Response delete(Object obj) {
243                 return method("DELETE", obj);
244         }
245
246         public <T> Optional<T> get(Class<T> resultClass) {
247                 return format(method("GET", null), resultClass);
248         }
249
250         public <T> Optional<T> get(GenericType<T> resultClass) {
251                 return format(method("GET", null), resultClass);
252         }
253
254         public <T> T post(Object obj, Class<T> resultClass) {
255                 return format(method("POST", obj), resultClass).orElse(null);
256         }
257
258         public <T> T patch(Object obj, Class<T> resultClass) {
259                 return format(method("PATCH", obj), resultClass).orElse(null);
260         }
261
262         public <T> T put(Object obj, Class<T> resultClass) {
263                 return format(method("PUT", obj), resultClass).orElse(null);
264         }
265
266         public <T> T delete(Class<T> resultClass) {
267                 return format(method("DELETE", null), resultClass).orElse(null);
268         }
269         
270         public <T> T delete(Object obj, Class<T> resultClass) {
271                 return format(method("DELETE", obj), resultClass).orElse(null);
272         }
273         
274         private Response method(String method, Object entity) {
275                 RetryPolicy policy = new RetryPolicy();
276                 
277                 List<Predicate<Throwable>> items = retryOn();
278                 
279                 Predicate<Throwable> pred = items.stream().reduce(Predicate::or).orElse(x -> false);
280
281                 policy.retryOn(error -> pred.test(error));
282                         
283                 policy.withDelay(this.props.getDelayBetweenRetries(), TimeUnit.MILLISECONDS)
284                                 .withMaxRetries(this.props.getRetries());
285                 
286                 return Failsafe.with(policy).get(buildRequest(method, entity));
287         }
288         
289         protected RestRequest buildRequest(String method, Object entity) {
290                 return new RestRequest(this, method, entity);
291         }
292         private <T> Optional<T> format(Response response, Class<T> resultClass) {
293                 if (this.props.mapNotFoundToEmpty() && response.getStatus() == Status.NOT_FOUND.getStatusCode()) {
294                         return Optional.empty();
295                 }
296                 return Optional.of(response.readEntity(resultClass));
297         }
298         
299         private <T> Optional<T> format(Response response, GenericType<T> resultClass) {
300                 if (this.props.mapNotFoundToEmpty() && response.getStatus() == Status.NOT_FOUND.getStatusCode()) {
301                         return Optional.empty();
302                 }
303                 return Optional.of(response.readEntity(resultClass));
304         }
305 }