001
002package io.vrap.rmf.base.client;
003
004import static io.vrap.rmf.base.client.utils.ClientUtils.blockingWait;
005
006import java.io.UnsupportedEncodingException;
007import java.net.URLEncoder;
008import java.nio.charset.StandardCharsets;
009import java.time.Duration;
010import java.util.ArrayList;
011import java.util.List;
012import java.util.Map;
013import java.util.concurrent.CompletableFuture;
014import java.util.function.BiFunction;
015import java.util.function.Function;
016import java.util.stream.Collectors;
017
018import javax.annotation.Nullable;
019
020import com.fasterxml.jackson.core.type.TypeReference;
021import com.fasterxml.jackson.databind.JavaType;
022
023import org.apache.commons.lang3.builder.EqualsBuilder;
024import org.apache.commons.lang3.builder.HashCodeBuilder;
025
026public abstract class ApiMethod<T extends ApiMethod<T, TResult>, TResult> extends Base
027        implements RequestCommand<TResult>, ClientRequestCommand<TResult> {
028
029    public static class ParamEntry<K, V> extends Base implements Map.Entry<K, V> {
030        protected final K key;
031        protected V value;
032
033        public ParamEntry(final K key) {
034            this.key = key;
035        }
036
037        public ParamEntry(final K key, final V value) {
038            this.key = key;
039            this.value = value;
040        }
041
042        @Override
043        public K getKey() {
044            return key;
045        }
046
047        @Override
048        public V getValue() {
049            return value;
050        }
051
052        @Override
053        public V setValue(final V value) {
054            V oldValue = this.value;
055            this.value = value;
056            return oldValue;
057        }
058
059        public String toUriString() {
060            try {
061                return key + "=" + URLEncoder.encode(value.toString(), StandardCharsets.UTF_8.toString());
062            }
063            catch (UnsupportedEncodingException e) {
064                throw new EncodingException(e);
065            }
066        }
067
068        @Override
069        public boolean equals(Object o) {
070            if (this == o)
071                return true;
072
073            if (o == null || getClass() != o.getClass())
074                return false;
075
076            ParamEntry<?, ?> that = (ParamEntry<?, ?>) o;
077
078            return new EqualsBuilder().append(key, that.key).append(value, that.value).isEquals();
079        }
080
081        @Override
082        public int hashCode() {
083            return new HashCodeBuilder(17, 37).append(key).append(value).toHashCode();
084        }
085    }
086
087    private Function<ApiHttpRequest, ApiHttpRequest> httpRequestDecorator = Function.identity();
088    private ApiHttpHeaders headers = new ApiHttpHeaders();
089    private List<ParamEntry<String, String>> queryParams = new ArrayList<>();
090    private final ApiHttpClient apiHttpClient;
091
092    protected ApiHttpClient apiHttpClient() {
093        return apiHttpClient;
094    }
095
096    public ApiMethod(final ApiHttpClient apiHttpClient) {
097        this.apiHttpClient = apiHttpClient;
098    }
099
100    public ApiMethod(final ApiHttpClient apiHttpClient, ApiHttpHeaders headers,
101            List<ParamEntry<String, String>> queryParams) {
102        this.apiHttpClient = apiHttpClient;
103        this.headers = headers;
104        this.queryParams = new ArrayList<>(queryParams);
105    }
106
107    public ApiMethod(final ApiMethod<T, TResult> apiMethod) {
108        this.apiHttpClient = apiMethod.apiHttpClient;
109        this.headers = new ApiHttpHeaders(apiMethod.headers);
110        this.queryParams = new ArrayList<>(apiMethod.queryParams);
111    }
112
113    /**
114     * adds an additional header with the specified value
115     * @param key header name
116     * @param value header value
117     * @return T
118     */
119    public T addHeader(final String key, final String value) {
120        final T c = copy();
121        ((ApiMethod<T, TResult>) c).headers = ((ApiMethod<T, TResult>) c).headers.addHeader(key, value);
122        return c;
123    }
124
125    /**
126     * removes the specified header
127     * @param key header name
128     * @return T
129     */
130    public T withoutHeader(final String key) {
131        final T c = copy();
132        ((ApiMethod<T, TResult>) c).headers = ((ApiMethod<T, TResult>) c).headers.withoutHeader(key);
133        return c;
134    }
135
136    /**
137     * set the header with the specified value
138     * @param key header name
139     * @param value header value
140     * @return T
141     */
142    public T withHeader(final String key, final String value) {
143        final T c = copy();
144        ((ApiMethod<T, TResult>) c).headers = ((ApiMethod<T, TResult>) c).headers.withHeader(key, value);
145        return c;
146    }
147
148    /**
149     * set the headers
150     * @param headers
151     * @return
152     */
153    public T withHeaders(final ApiHttpHeaders headers) {
154        final T c = copy();
155        ((ApiMethod<T, TResult>) c).headers = ((ApiMethod<T, TResult>) c).headers = headers;
156        return c;
157    }
158
159    /**
160     * set specific content type
161     * @param contentType
162     * @return
163     */
164    public T contentType(final String contentType) {
165        final T c = copy();
166        ((ApiMethod<T, TResult>) c).headers = ((ApiMethod<T, TResult>) c).headers
167                .withHeader(ApiHttpHeaders.CONTENT_TYPE, contentType);
168
169        return c;
170    }
171
172    public ApiHttpHeaders getHeaders() {
173        return this.headers;
174    }
175
176    /**
177     * add an additional query parameter
178     * @param key query parameter name
179     * @param value query parameter value
180     * @param <V> value type
181     * @return T
182     */
183    public <V> T addQueryParam(final String key, final V value) {
184        final T c = copy();
185        ((ApiMethod<T, TResult>) c).queryParams.add(new ParamEntry<>(key, value.toString()));
186        return c;
187    }
188
189    /**
190     * set the query parameter with the specified value
191     * @param key query parameter name
192     * @param value query parameter value
193     * @param <V> value type
194     * @return T
195     */
196    public <V> T withQueryParam(final String key, final V value) {
197        return withoutQueryParam(key).addQueryParam(key, value);
198    }
199
200    /**
201     * removes the specified query parameter
202     * @param key query parameter name
203     * @return T
204     */
205    public T withoutQueryParam(final String key) {
206        final T c = copy();
207        ((ApiMethod<T, TResult>) c).queryParams = ((ApiMethod<T, TResult>) c).queryParams.stream()
208                .filter(e -> !e.getKey().equalsIgnoreCase(key))
209                .collect(Collectors.toList());
210        return c;
211    }
212
213    /**
214     * set the query parameters
215     * @param queryParams list of query parameters
216     * @return T
217     */
218    public T withQueryParams(final List<ParamEntry<String, String>> queryParams) {
219        final T c = copy();
220        ((ApiMethod<T, TResult>) c).queryParams = queryParams;
221        return c;
222    }
223
224    /**
225     * add the query parameters
226     * @param queryParams list of query parameters
227     * @return T
228     */
229    public T addQueryParams(final List<ParamEntry<String, String>> queryParams) {
230        final T c = copy();
231
232        ((ApiMethod<T, TResult>) c).queryParams.addAll(queryParams);
233        return c;
234    }
235
236    public List<ParamEntry<String, String>> getQueryParams() {
237        return new ArrayList<>(this.queryParams);
238    }
239
240    public List<String> getQueryParam(final String key) {
241        return this.queryParams.stream().filter(e -> e.key.equals(key)).map(e -> e.value).collect(Collectors.toList());
242    }
243
244    public List<String> getQueryParamUriStrings() {
245        return this.queryParams.stream().map(ParamEntry::toUriString).collect(Collectors.toList());
246    }
247
248    public String getQueryParamUriString() {
249        return this.queryParams.stream().map(ParamEntry::toUriString).collect(Collectors.joining("&"));
250    }
251
252    @Nullable
253    public String getFirstQueryParam(final String key) {
254        return this.queryParams.stream()
255                .filter(e -> e.getKey().equals(key))
256                .map(Map.Entry::getValue)
257                .findFirst()
258                .orElse(null);
259    }
260
261    protected abstract T copy();
262
263    protected abstract ApiHttpRequest buildHttpRequest();
264
265    public ApiHttpRequest createHttpRequest() {
266        return httpRequestDecorator.apply(this.buildHttpRequest());
267    }
268
269    public CompletableFuture<ApiHttpResponse<TResult>> execute() {
270        return execute(apiHttpClient());
271    }
272
273    public abstract CompletableFuture<ApiHttpResponse<TResult>> execute(final ApiHttpClient client);
274
275    /**
276     * allows to modify the HTTP request before it will be executed
277     * @param op decorator function
278     * @return the method itself
279     */
280    public T withHttpRequest(Function<ApiHttpRequest, ApiHttpRequest> op) {
281        final T c = copy();
282        ((ApiMethod<T, TResult>) c).httpRequestDecorator = httpRequestDecorator.andThen(op);
283        return c;
284    }
285
286    /**
287     * allows to provide a function to modify the ApiMethod itself
288     * @param op decorator function
289     * @return the method itself
290     */
291    public T with(Function<T, T> op) {
292        final T c = copy();
293        return op.apply(c);
294    }
295
296    /**
297     * allows to provide a function to modify the ApiMethod itself
298     * @param op decorator function
299     * @param arg decorator function argument
300     * @param <U> the method type itself
301     * @return the method itself
302     */
303    public <U> T with(BiFunction<T, U, T> op, U arg) {
304        final T c = copy();
305        return op.apply(c, arg);
306    }
307
308    public <TReturn> CompletableFuture<ApiHttpResponse<TReturn>> execute(final Class<TReturn> returnType) {
309        return execute(apiHttpClient(), returnType);
310    }
311
312    public <TReturn> CompletableFuture<ApiHttpResponse<TReturn>> execute(final ApiHttpClient client,
313            final Class<TReturn> returnType) {
314        return client.execute(this.createHttpRequest(), returnType).toCompletableFuture();
315    }
316
317    public <TReturn> CompletableFuture<ApiHttpResponse<TReturn>> execute(
318            final TypeReference<TReturn> returnTypeReference) {
319        return execute(apiHttpClient(), returnTypeReference);
320    }
321
322    public <TReturn> CompletableFuture<ApiHttpResponse<TReturn>> execute(final ApiHttpClient client,
323            final TypeReference<TReturn> returnTypeReference) {
324        return client.execute(this.createHttpRequest(), returnTypeReference).toCompletableFuture();
325    }
326
327    public <TReturn> CompletableFuture<ApiHttpResponse<TReturn>> execute(final JavaType returnJavaType) {
328        return execute(apiHttpClient(), returnJavaType);
329    }
330
331    public <TReturn> CompletableFuture<ApiHttpResponse<TReturn>> execute(final ApiHttpClient client,
332            final JavaType returnJavaType) {
333        return client.execute(this.createHttpRequest(), returnJavaType);
334    }
335
336    public ApiHttpResponse<TResult> executeBlocking() {
337        return executeBlocking(apiHttpClient(), Duration.ofSeconds(120));
338    };
339
340    public ApiHttpResponse<TResult> executeBlocking(final ApiHttpClient client) {
341        return executeBlocking(client, Duration.ofSeconds(120));
342    };
343
344    public ApiHttpResponse<TResult> executeBlocking(final Duration timeout) {
345        return executeBlocking(apiHttpClient(), timeout);
346    }
347
348    public abstract ApiHttpResponse<TResult> executeBlocking(final ApiHttpClient client, final Duration timeout);
349
350    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final Class<TReturn> clazz) {
351        return executeBlocking(apiHttpClient(), Duration.ofSeconds(120), clazz);
352    };
353
354    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final ApiHttpClient client, final Class<TReturn> clazz) {
355        return executeBlocking(client, Duration.ofSeconds(120), clazz);
356    };
357
358    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final Duration timeout, final Class<TReturn> clazz) {
359        return executeBlocking(apiHttpClient(), timeout, clazz);
360    }
361
362    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final ApiHttpClient client, final Duration timeout,
363            final Class<TReturn> clazz) {
364        final ApiHttpRequest request = this.createHttpRequest();
365        return blockingWait(client.execute(request, clazz).toCompletableFuture(), request, timeout);
366    }
367
368    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final TypeReference<TReturn> typeReference) {
369        return executeBlocking(apiHttpClient(), Duration.ofSeconds(120), typeReference);
370    };
371
372    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final ApiHttpClient client,
373            final TypeReference<TReturn> typeReference) {
374        return executeBlocking(client, Duration.ofSeconds(120), typeReference);
375    };
376
377    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final Duration timeout,
378            final TypeReference<TReturn> typeReference) {
379        return executeBlocking(apiHttpClient(), timeout, typeReference);
380    }
381
382    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final ApiHttpClient client, final Duration timeout,
383            TypeReference<TReturn> typeReference) {
384        final ApiHttpRequest request = this.createHttpRequest();
385        return blockingWait(client.execute(request, typeReference).toCompletableFuture(), request, timeout);
386    }
387
388    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final JavaType javaType) {
389        return executeBlocking(apiHttpClient(), Duration.ofSeconds(120), javaType);
390    }
391
392    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final ApiHttpClient client, final JavaType javaType) {
393        return executeBlocking(client, Duration.ofSeconds(120), javaType);
394    }
395
396    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final Duration timeout, final JavaType javaType) {
397        return executeBlocking(apiHttpClient(), timeout, javaType);
398    }
399
400    public <TReturn> ApiHttpResponse<TReturn> executeBlocking(final ApiHttpClient client, final Duration timeout,
401            JavaType javaType) {
402        final ApiHttpRequest request = this.createHttpRequest();
403        return blockingWait(client.execute(request, javaType), request, timeout);
404    }
405
406    public CompletableFuture<ApiHttpResponse<byte[]>> send() {
407        return apiHttpClient.execute(createHttpRequest());
408    }
409
410    public ApiHttpResponse<byte[]> sendBlocking() {
411        return sendBlocking(Duration.ofSeconds(120));
412    };
413
414    public ApiHttpResponse<byte[]> sendBlocking(final Duration timeout) {
415        return blockingWait(send(), timeout);
416    }
417}