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