001package ball.http;
002/*-
003 * ##########################################################################
004 * Web API Client (HTTP) Utilities
005 * $Id: ProtocolRequestBuilder.java 6118 2020-06-04 19:31:45Z ball $
006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-http/trunk/src/main/java/ball/http/ProtocolRequestBuilder.java $
007 * %%
008 * Copyright (C) 2016 - 2020 Allen D. Ball
009 * %%
010 * Licensed under the Apache License, Version 2.0 (the "License");
011 * you may not use this file except in compliance with the License.
012 * You may obtain a copy of the License at
013 *
014 *      http://www.apache.org/licenses/LICENSE-2.0
015 *
016 * Unless required by applicable law or agreed to in writing, software
017 * distributed under the License is distributed on an "AS IS" BASIS,
018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
019 * See the License for the specific language governing permissions and
020 * limitations under the License.
021 * ##########################################################################
022 */
023import ball.activation.ByteArrayDataSource;
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.OutputStream;
028import java.lang.annotation.Annotation;
029import java.lang.reflect.InvocationTargetException;
030import java.lang.reflect.Method;
031import java.lang.reflect.Parameter;
032import java.net.URI;
033import java.util.ArrayList;
034import java.util.Arrays;
035import java.util.Collections;
036import java.util.TreeMap;
037import java.util.Set;
038import java.util.stream.Collectors;
039import javax.ws.rs.ApplicationPath;
040import javax.ws.rs.BeanParam;
041import javax.ws.rs.ConstrainedTo;
042import javax.ws.rs.Consumes;
043import javax.ws.rs.CookieParam;
044/* import javax.ws.rs.DefaultValue; */
045import javax.ws.rs.DELETE;
046/* import javax.ws.rs.Encoded; */
047import javax.ws.rs.FormParam;
048import javax.ws.rs.GET;
049import javax.ws.rs.HEAD;
050import javax.ws.rs.HeaderParam;
051import javax.ws.rs.MatrixParam;
052import javax.ws.rs.OPTIONS;
053import javax.ws.rs.PATCH;
054import javax.ws.rs.POST;
055import javax.ws.rs.PUT;
056import javax.ws.rs.Path;
057import javax.ws.rs.PathParam;
058import javax.ws.rs.Produces;
059import javax.ws.rs.QueryParam;
060import javax.ws.rs.core.UriBuilder;
061import lombok.ToString;
062import org.apache.commons.lang3.ClassUtils;
063import org.apache.commons.lang3.reflect.MethodUtils;
064import org.apache.http.HttpEntity;
065import org.apache.http.HttpMessage;
066import org.apache.http.HttpRequest;
067import org.apache.http.NameValuePair;
068import org.apache.http.client.entity.UrlEncodedFormEntity;
069import org.apache.http.client.methods.HttpDelete;
070import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
071import org.apache.http.client.methods.HttpGet;
072import org.apache.http.client.methods.HttpHead;
073import org.apache.http.client.methods.HttpOptions;
074import org.apache.http.client.methods.HttpPatch;
075import org.apache.http.client.methods.HttpPost;
076import org.apache.http.client.methods.HttpPut;
077import org.apache.http.client.methods.HttpRequestBase;
078import org.apache.http.entity.AbstractHttpEntity;
079import org.apache.http.entity.ContentType;
080import org.apache.http.message.BasicNameValuePair;
081
082import static java.util.Objects.requireNonNull;
083import static org.apache.commons.lang3.StringUtils.EMPTY;
084import static org.apache.commons.lang3.StringUtils.isBlank;
085import static org.apache.commons.lang3.StringUtils.isNotBlank;
086
087/**
088 * <p>
089 * {@link HttpRequest} builder for {@link ProtocolClient#protocol()}.  See
090 * the {@code type(Annotation,Class)}, {@code method(Annotation,Method)},
091 * {@code parameter(Annotation,Parameter,...)},
092 * and {@code parameter(Parameter,...)} methods for the supported protocol
093 * interface, method, and method parameter {@link Annotation}s and types.
094 * </p>
095 * <p>
096 * Protocol API authors should consider designing protocol methods to throw
097 * {@link org.apache.http.client.HttpResponseException},
098 * {@link org.apache.http.client.ClientProtocolException}, and
099 * {@link java.io.IOException}.
100 * </p>
101 * <p>
102 * Supported type (interface) annotations:
103 *
104 * {@include #TYPE_ANNOTATIONS}
105 * </p>
106 * <p>
107 * Supported method annotations:
108 *
109 * {@include #METHOD_ANNOTATIONS}
110 * </p>
111 * <p>
112 * Supported method parameter annotations:
113 *
114 * {@include #PARAMETER_ANNOTATIONS}
115 * </p>
116 * <p>
117 * Supported method parameter types:
118 *
119 * {@include #PARAMETER_TYPES}
120 * </p>
121 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
122 * @version $Revision: 6118 $
123 */
124@ToString
125public class ProtocolRequestBuilder {
126
127    /**
128     * Supported type (interface) annotations.
129     */
130    public static final Set<Class<? extends Annotation>> TYPE_ANNOTATIONS =
131        Arrays.stream(ProtocolRequestBuilder.class.getDeclaredMethods())
132        .filter(t -> t.getName().equals("type"))
133        .filter(t -> t.getParameterCount() > 0)
134        .filter(t -> Annotation.class.isAssignableFrom(t.getParameterTypes()[0]))
135        .filter(t -> ClassUtils.isAssignable(new Class<?>[] {
136                                                 t.getParameterTypes()[0],
137                                                 Class.class
138                                             },
139                                             t.getParameterTypes()))
140        .map(t -> t.getParameterTypes()[0].asSubclass(Annotation.class))
141        .collect(Collectors.toSet());
142
143    /**
144     * Supported method annotations.
145     */
146    public static final Set<Class<? extends Annotation>> METHOD_ANNOTATIONS =
147        Arrays.stream(ProtocolRequestBuilder.class.getDeclaredMethods())
148        .filter(t -> t.getName().equals("method"))
149        .filter(t -> t.getParameterCount() > 0)
150        .filter(t -> Annotation.class.isAssignableFrom(t.getParameterTypes()[0]))
151        .filter(t -> ClassUtils.isAssignable(new Class<?>[] {
152                                                 t.getParameterTypes()[0],
153                                                 Method.class
154                                             },
155                                             t.getParameterTypes()))
156        .map(t -> t.getParameterTypes()[0].asSubclass(Annotation.class))
157        .collect(Collectors.toSet());
158
159    /**
160     * Supported method parameter annotations.
161     */
162    public static final Set<Class<? extends Annotation>> PARAMETER_ANNOTATIONS =
163        Arrays.stream(ProtocolRequestBuilder.class.getDeclaredMethods())
164        .filter(t -> t.getName().equals("parameter"))
165        .filter(t -> t.getParameterCount() > 0)
166        .filter(t -> Annotation.class.isAssignableFrom(t.getParameterTypes()[0]))
167        .filter(t -> ClassUtils.isAssignable(new Class<?>[] { t.getParameterTypes()[0], Parameter.class, null },
168                                             t.getParameterTypes()))
169        .map(t -> t.getParameterTypes()[0].asSubclass(Annotation.class))
170        .collect(Collectors.toSet());
171
172    /**
173     * Supported method parameter types.
174     */
175    public static final Set<Class<?>> PARAMETER_TYPES =
176        Arrays.stream(ProtocolRequestBuilder.class.getDeclaredMethods())
177        .filter(t -> t.getName().equals("parameter"))
178        .filter(t -> t.getParameterCount() > 0)
179        .filter(t -> ClassUtils.isAssignable(new Class<?>[] { Parameter.class, null },
180                                             t.getParameterTypes()))
181        .map(t -> t.getParameterTypes()[1])
182        .collect(Collectors.toSet());
183
184    private final ProtocolClient<?> client;
185    private transient HttpMessage request = null;
186    private transient UriBuilder uri = UriBuilder.fromUri(EMPTY);
187    private transient TreeMap<String,Object> templateValues = new TreeMap<>();
188    private transient Object body = null;
189
190    /**
191     * Sole constructor.
192     *
193     * @param   client          The {@link ProtocolClient}.
194     */
195    protected ProtocolRequestBuilder(ProtocolClient<?> client) {
196        this.client = requireNonNull(client, "client");
197    }
198
199    /**
200     * Build a {@link HttpRequest} ({@link HttpMessage}) from the protocol
201     * interface {@link Method}.
202     *
203     * @param   method          The interface {@link Method}.
204     * @param   argv            The caller's arguments.
205     *
206     * @return  The {@link HttpMessage}.
207     *
208     * @throws  Throwable       If the call fails for any reason.
209     */
210    public HttpMessage build(Method method, Object[] argv) throws Throwable {
211        /*
212         * Process annotations and arguments
213         */
214        process(method.getDeclaringClass(), method, argv);
215        /*
216         * URI
217         */
218        if (request instanceof HttpRequestBase) {
219            ((HttpRequestBase) request)
220                .setURI(uri.resolveTemplates(templateValues).build());
221        }
222        /*
223         * Body
224         */
225        HttpEntity entity = null;
226
227        if (body instanceof HttpEntity) {
228            entity = (HttpEntity) body;
229        } else if (body instanceof Form) {
230            entity =
231                new UrlEncodedFormEntity((Form) body, client.getCharset());
232        } else if (body != null) {
233            entity = new JSONHttpEntity(body);
234        }
235
236        if (entity != null) {
237            ((HttpEntityEnclosingRequestBase) request).setEntity(entity);
238        }
239
240        return request;
241    }
242
243    private void process(Class<?> type,
244                         Method method, Object... argv) throws Throwable {
245        for (Annotation annotation : type.getAnnotations()) {
246            try {
247                if (TYPE_ANNOTATIONS.contains(annotation.annotationType())) {
248                    invoke("type",
249                           new Object[] { annotation, type },
250                           annotation.annotationType(), Class.class);
251                }
252            } catch (NoSuchMethodException exception) {
253                throw new IllegalStateException(String.valueOf(annotation.annotationType()),
254                                                exception);
255            } catch (IllegalAccessException exception) {
256            } catch (InvocationTargetException exception) {
257                throw exception.getTargetException();
258            }
259        }
260
261        for (Annotation annotation : method.getAnnotations()) {
262            try {
263                if (METHOD_ANNOTATIONS.contains(annotation.annotationType())) {
264                    invoke("method",
265                           new Object[] { annotation, method },
266                           annotation.annotationType(), Method.class);
267                }
268            } catch (NoSuchMethodException exception) {
269                throw new IllegalStateException(String.valueOf(annotation.annotationType()),
270                                                exception);
271            } catch (IllegalAccessException exception) {
272            } catch (InvocationTargetException exception) {
273                throw exception.getTargetException();
274            }
275        }
276
277        Parameter[] parameters = method.getParameters();
278
279        for (int i = 0; i < parameters.length; i += 1) {
280            process(parameters[i], argv[i]);
281        }
282    }
283
284    private void process(Parameter parameter,
285                         Object argument) throws Throwable {
286        Annotation[] annotations = parameter.getAnnotations();
287
288        if (annotations.length > 0) {
289            for (int i = 0; i < annotations.length; i += 1) {
290                process(annotations[i], parameter, argument);
291            }
292        } else {
293            try {
294                invoke("parameter",
295                       new Object[] { parameter, argument },
296                       Parameter.class, parameter.getType());
297            } catch (NoSuchMethodException exception) {
298            } catch (IllegalAccessException exception) {
299            } catch (InvocationTargetException exception) {
300                throw exception.getTargetException();
301            }
302        }
303    }
304
305    private void process(Annotation annotation,
306                         Parameter parameter,
307                         Object argument) throws Throwable {
308        try {
309            if (PARAMETER_ANNOTATIONS.contains(annotation.annotationType())) {
310                invoke("parameter",
311                       new Object[] { annotation, parameter, argument },
312                       annotation.annotationType(), Parameter.class, parameter.getType());
313            }
314        } catch (NoSuchMethodException exception) {
315            throw new IllegalStateException(String.valueOf(annotation.annotationType()),
316                                            exception);
317        } catch (IllegalAccessException exception) {
318        } catch (InvocationTargetException exception) {
319            throw exception.getTargetException();
320        }
321    }
322
323    private void invoke(String name, Object[] argv,
324                        Class<?>... parameters) throws Throwable {
325        MethodUtils.invokeMethod(this, true, name, argv, parameters);
326    }
327
328    /**
329     * {@link ApplicationPath} type (interface) {@link Annotation}
330     *
331     * @param   annotation      The {@link ApplicationPath}
332     *                          {@link Annotation}.
333     * @param   type            The annotated {@link Class}.
334     *
335     * @throws  Throwable       If the {@link Annotation} cannot be
336     *                          configured.
337     */
338    protected void type(ApplicationPath annotation,
339                        Class<?> type) throws Throwable {
340        uri = uri.uri(annotation.value());
341    }
342
343    /**
344     * {@link ConstrainedTo} type (interface) {@link Annotation}
345     *
346     * @param   annotation      The {@link ConstrainedTo}
347     *                          {@link Annotation}.
348     * @param   type            The annotated {@link Class}.
349     *
350     * @throws  Throwable       If the {@link Annotation} cannot be
351     *                          configured.
352     */
353    protected void type(ConstrainedTo annotation,
354                        Class<?> type) throws Throwable {
355        throw new UnsupportedOperationException(annotation.toString());
356    }
357
358    /**
359     * {@link Consumes} type (interface) {@link Annotation}
360     *
361     * @param   annotation      The {@link Consumes} {@link Annotation}.
362     * @param   type            The annotated {@link Class}.
363     *
364     * @throws  Throwable       If the {@link Annotation} cannot be
365     *                          configured.
366     */
367    protected void type(Consumes annotation, Class<?> type) throws Throwable {
368        throw new UnsupportedOperationException(annotation.toString());
369    }
370
371    /**
372     * {@link Path} type (interface) {@link Annotation}
373     *
374     * @param   annotation      The {@link Path} {@link Annotation}.
375     * @param   type            The annotated {@link Class}.
376     *
377     * @throws  Throwable       If the {@link Annotation} cannot be
378     *                          configured.
379     */
380    protected void type(Path annotation, Class<?> type) throws Throwable {
381        uri = uri.path(annotation.value());
382    }
383
384    /**
385     * {@link Produces} type (interface) {@link Annotation}
386     *
387     * @param   annotation      The {@link Produces} {@link Annotation}.
388     * @param   type            The annotated {@link Class}.
389     *
390     * @throws  Throwable       If the {@link Annotation} cannot be
391     *                          configured.
392     */
393    protected void type(Produces annotation, Class<?> type) throws Throwable {
394        throw new UnsupportedOperationException(annotation.toString());
395    }
396
397    /**
398     * {@link BeanParam} method {@link Annotation}
399     *
400     * @param   annotation      The {@link BeanParam}
401     *                          {@link Annotation}.
402     * @param   method          The annotated {@link Method}.
403     *
404     * @throws  Throwable       If the {@link Annotation} cannot be
405     *                          configured.
406     */
407    protected void method(BeanParam annotation,
408                          Method method) throws Throwable {
409        throw new UnsupportedOperationException(annotation.toString());
410    }
411
412    /**
413     * {@link Consumes} method {@link Annotation}
414     *
415     * @param   annotation      The {@link Consumes} {@link Annotation}.
416     * @param   method          The annotated {@link Method}.
417     *
418     * @throws  Throwable       If the {@link Annotation} cannot be
419     *                          configured.
420     */
421    protected void method(Consumes annotation,
422                          Method method) throws Throwable {
423        throw new UnsupportedOperationException(annotation.toString());
424    }
425
426    /**
427     * {@link CookieParam} method {@link Annotation}
428     *
429     * @param   annotation      The {@link CookieParam} {@link Annotation}.
430     * @param   method          The annotated {@link Method}.
431     *
432     * @throws  Throwable       If the {@link Annotation} cannot be
433     *                          configured.
434     */
435    protected void method(CookieParam annotation,
436                          Method method) throws Throwable {
437        throw new UnsupportedOperationException(annotation.toString());
438    }
439
440    /**
441     * {@link DELETE} method {@link Annotation}
442     *
443     * @param   annotation      The {@link DELETE} {@link Annotation}.
444     * @param   method          The annotated {@link Method}.
445     *
446     * @throws  Throwable       If the {@link Annotation} cannot be
447     *                          configured.
448     */
449    protected void method(DELETE annotation, Method method) throws Throwable {
450        request = new HttpDelete();
451    }
452
453    /**
454     * {@link FormParam} method {@link Annotation}
455     *
456     * @param   annotation      The {@link FormParam} {@link Annotation}.
457     * @param   method          The annotated {@link Method}.
458     *
459     * @throws  Throwable       If the {@link Annotation} cannot be
460     *                          configured.
461     */
462    protected void method(FormParam annotation,
463                          Method method) throws Throwable {
464        throw new UnsupportedOperationException(annotation.toString());
465    }
466
467    /**
468     * {@link GET} method {@link Annotation}
469     *
470     * @param   annotation      The {@link GET} {@link Annotation}.
471     * @param   method          The annotated {@link Method}.
472     *
473     * @throws  Throwable       If the {@link Annotation} cannot be
474     *                          configured.
475     */
476    protected void method(GET annotation, Method method) throws Throwable {
477        request = new HttpGet();
478    }
479
480    /**
481     * {@link HEAD} method {@link Annotation}
482     *
483     * @param   annotation      The {@link HEAD} {@link Annotation}.
484     * @param   method          The annotated {@link Method}.
485     *
486     * @throws  Throwable       If the {@link Annotation} cannot be
487     *                          configured.
488     */
489    protected void method(HEAD annotation, Method method) throws Throwable {
490        request = new HttpHead();
491    }
492
493    /**
494     * {@link HeaderParam} method {@link Annotation}
495     *
496     * @param   annotation      The {@link HeaderParam} {@link Annotation}.
497     * @param   method          The annotated {@link Method}.
498     *
499     * @throws  Throwable       If the {@link Annotation} cannot be
500     *                          configured.
501     */
502    protected void method(HeaderParam annotation,
503                          Method method) throws Throwable {
504        throw new UnsupportedOperationException(annotation.toString());
505    }
506
507    /**
508     * {@link MatrixParam} method {@link Annotation}
509     *
510     * @param   annotation      The {@link MatrixParam} {@link Annotation}.
511     * @param   method          The annotated {@link Method}.
512     *
513     * @throws  Throwable       If the {@link Annotation} cannot be
514     *                          configured.
515     */
516    protected void method(MatrixParam annotation,
517                          Method method) throws Throwable {
518        throw new UnsupportedOperationException(annotation.toString());
519    }
520
521    /**
522     * {@link OPTIONS} method {@link Annotation}
523     *
524     * @param   annotation      The {@link OPTIONS} {@link Annotation}.
525     * @param   method          The annotated {@link Method}.
526     *
527     * @throws  Throwable       If the {@link Annotation} cannot be
528     *                          configured.
529     */
530    protected void method(OPTIONS annotation, Method method) throws Throwable {
531        request = new HttpOptions();
532    }
533
534    /**
535     * {@link PATCH} method {@link Annotation}
536     *
537     * @param   annotation      The {@link PATCH} {@link Annotation}.
538     * @param   method          The annotated {@link Method}.
539     *
540     * @throws  Throwable       If the {@link Annotation} cannot be
541     *                          configured.
542     */
543    protected void method(PATCH annotation, Method method) throws Throwable {
544        request = new HttpPatch();
545    }
546
547    /**
548     * {@link Path} method {@link Annotation}
549     *
550     * @param   annotation      The {@link Path} {@link Annotation}.
551     * @param   method          The annotated {@link Method}.
552     *
553     * @throws  Throwable       If the {@link Annotation} cannot be
554     *                          configured.
555     */
556    protected void method(Path annotation, Method method) throws Throwable {
557        uri = uri.path(annotation.value());
558    }
559
560    /**
561     * {@link PathParam} method {@link Annotation}
562     *
563     * @param   annotation      The {@link PathParam} {@link Annotation}.
564     * @param   method          The annotated {@link Method}.
565     *
566     * @throws  Throwable       If the {@link Annotation} cannot be
567     *                          configured.
568     */
569    protected void method(PathParam annotation,
570                          Method method) throws Throwable {
571        throw new UnsupportedOperationException(annotation.toString());
572    }
573
574    /**
575     * {@link QueryParam} method {@link Annotation}
576     *
577     * @param   annotation      The {@link QueryParam} {@link Annotation}.
578     * @param   method          The annotated {@link Method}.
579     *
580     * @throws  Throwable       If the {@link Annotation} cannot be
581     *                          configured.
582     */
583    protected void method(QueryParam annotation,
584                          Method method) throws Throwable {
585        throw new UnsupportedOperationException(annotation.toString());
586    }
587
588    /**
589     * {@link POST} method {@link Annotation}
590     *
591     * @param   annotation      The {@link POST} {@link Annotation}.
592     * @param   method          The annotated {@link Method}.
593     *
594     * @throws  Throwable       If the {@link Annotation} cannot be
595     *                          configured.
596     */
597    protected void method(POST annotation, Method method) throws Throwable {
598        request = new HttpPost();
599    }
600
601    /**
602     * {@link Produces} method {@link Annotation}
603     *
604     * @param   annotation      The {@link Produces} {@link Annotation}.
605     * @param   method          The annotated {@link Method}.
606     *
607     * @throws  Throwable       If the {@link Annotation} cannot be
608     *                          configured.
609     */
610    protected void method(Produces annotation,
611                          Method method) throws Throwable {
612        throw new UnsupportedOperationException(annotation.toString());
613    }
614
615    /**
616     * {@link PUT} method {@link Annotation}
617     *
618     * @param   annotation      The {@link PUT} {@link Annotation}.
619     * @param   method          The annotated {@link Method}.
620     *
621     * @throws  Throwable       If the {@link Annotation} cannot be
622     *                          configured.
623     */
624    protected void method(PUT annotation, Method method) throws Throwable {
625        request = new HttpPut();
626    }
627
628    /**
629     * {@link BeanParam} method parameter {@link Annotation}
630     *
631     * @param   annotation      The {@link BeanParam} {@link Annotation}.
632     * @param   parameter       The {@link Method} {@link Parameter}.
633     * @param   argument        The {@link Object} representing the cookie
634     *                          parameter value.
635     *
636     * @throws  Throwable       If the {@link Annotation} cannot be
637     *                          configured.
638     */
639    protected void parameter(BeanParam annotation,
640                             Parameter parameter,
641                             Object argument) throws Throwable {
642        if (argument != null) {
643            throw new UnsupportedOperationException(annotation.toString());
644        }
645    }
646
647    /**
648     * {@link CookieParam} method parameter {@link Annotation}
649     *
650     * @param   annotation      The {@link CookieParam} {@link Annotation}.
651     * @param   parameter       The {@link Method} {@link Parameter}.
652     * @param   argument        The {@link Object} representing the cookie
653     *                          parameter value.
654     *
655     * @throws  Throwable       If the {@link Annotation} cannot be
656     *                          configured.
657     */
658    protected void parameter(CookieParam annotation,
659                             Parameter parameter,
660                             Object argument) throws Throwable {
661        throw new UnsupportedOperationException(annotation.toString());
662    }
663
664    /**
665     * {@link FormParam} method parameter {@link Annotation}
666     *
667     * @param   annotation      The {@link FormParam} {@link Annotation}.
668     * @param   parameter       The {@link Method} {@link Parameter}.
669     * @param   argument        The {@link Object} representing the form
670     *                          parameter value.
671     *
672     * @throws  Throwable       If the {@link Annotation} cannot be
673     *                          configured.
674     */
675    protected void parameter(FormParam annotation,
676                             Parameter parameter,
677                             Object argument) throws Throwable {
678        String name =
679            isNotBlank(annotation.value())
680                ? annotation.value()
681                : parameter.getName();
682
683        if (argument != null) {
684            if (body == null) {
685                body = new Form();
686            }
687
688            ((Form) body).add(name, String.valueOf(argument));
689        }
690    }
691
692    /**
693     * {@link HeaderParam} method parameter {@link Annotation}
694     *
695     * @param   annotation      The {@link HeaderParam} {@link Annotation}.
696     * @param   parameter       The {@link Method} {@link Parameter}.
697     * @param   argument        The {@link Object} representing the header
698     *                          parameter value.
699     *
700     * @throws  Throwable       If the {@link Annotation} cannot be
701     *                          configured.
702     */
703    protected void parameter(HeaderParam annotation,
704                             Parameter parameter,
705                             Object argument) throws Throwable {
706        String name =
707            isNotBlank(annotation.value())
708                ? annotation.value()
709                : parameter.getName();
710
711        if (argument != null) {
712            request.setHeader(name, String.valueOf(argument));
713        }
714    }
715
716    /**
717     * {@link MatrixParam} method parameter {@link Annotation}
718     *
719     * @param   annotation      The {@link MatrixParam} {@link Annotation}.
720     * @param   parameter       The {@link Method} {@link Parameter}.
721     * @param   argument        The {@link Object} representing the matrix
722     *                          parameter value.
723     *
724     * @throws  Throwable       If the {@link Annotation} cannot be
725     *                          configured.
726     */
727    protected void parameter(MatrixParam annotation,
728                             Parameter parameter,
729                             Object argument) throws Throwable {
730        String name =
731            isNotBlank(annotation.value())
732                ? annotation.value()
733                : parameter.getName();
734
735        uri = uri.replaceMatrixParam(name, argument);
736    }
737
738    /**
739     * {@link PathParam} method parameter {@link Annotation}
740     *
741     * @param   annotation      The {@link PathParam} {@link Annotation}.
742     * @param   parameter       The {@link Method} {@link Parameter}.
743     * @param   argument        The {@link Object} representing the path
744     *                          parameter value.
745     *
746     * @throws  Throwable       If the {@link Annotation} cannot be
747     *                          configured.
748     */
749    protected void parameter(PathParam annotation,
750                             Parameter parameter,
751                             Object argument) throws Throwable {
752        String name =
753            isNotBlank(annotation.value())
754                ? annotation.value()
755                : parameter.getName();
756
757        if (argument != null) {
758            templateValues.put(name, String.valueOf(argument));
759        } else {
760            templateValues.remove(name);
761        }
762    }
763
764    /**
765     * {@link QueryParam} method parameter {@link Annotation}
766     *
767     * @param   annotation      The {@link QueryParam} {@link Annotation}.
768     * @param   parameter       The {@link Method} {@link Parameter}.
769     * @param   argument        The {@link Object} representing the query
770     *                          parameter value.
771     *
772     * @throws  Throwable       If the {@link Annotation} cannot be
773     *                          configured.
774     */
775    protected void parameter(QueryParam annotation,
776                             Parameter parameter,
777                             Object argument) throws Throwable {
778        String name =
779            isNotBlank(annotation.value())
780                ? annotation.value()
781                : parameter.getName();
782
783        uri = uri.replaceQueryParam(name, argument);
784    }
785
786    /**
787     * {@link HttpMessage} method parameter
788     *
789     * @param   parameter       The {@link Method} {@link Parameter}.
790     * @param   argument        The {@link HttpMessage}.
791     *
792     * @throws  Throwable       If the argument cannot be configured.
793     */
794    protected void parameter(Parameter parameter,
795                             HttpMessage argument) throws Throwable {
796        request = argument;
797    }
798
799    /**
800     * {@link HttpEntity} method parameter
801     *
802     * @param   parameter       The {@link Method} {@link Parameter}.
803     * @param   argument        The {@link HttpEntity}.
804     *
805     * @throws  Throwable       If the argument cannot be configured.
806     */
807    protected void parameter(Parameter parameter,
808                             HttpEntity argument) throws Throwable {
809        body = argument;
810    }
811
812    /**
813     * {@link URI} method parameter
814     *
815     * @param   parameter       The {@link Method} {@link Parameter}.
816     * @param   argument        The {@link URI}.
817     *
818     * @throws  Throwable       If the argument cannot be configured.
819     */
820    protected void parameter(Parameter parameter,
821                             URI argument) throws Throwable {
822        uri = uri.uri(argument);
823    }
824
825    /**
826     * {@link Object} method parameter
827     *
828     * @param   parameter       The {@link Method} {@link Parameter}.
829     * @param   argument        The {@link Object}.
830     *
831     * @throws  Throwable       If the argument cannot be configured.
832     */
833    protected void parameter(Parameter parameter,
834                             Object argument) throws Throwable {
835        body = argument;
836    }
837
838    private class Form extends ArrayList<NameValuePair> {
839        private static final long serialVersionUID = -738222384949508109L;
840
841        public Form() { super(); }
842
843        public boolean add(String name, String value) {
844            return add(new BasicNameValuePair(name, value));
845        }
846    }
847
848    private abstract class HttpEntityImpl extends AbstractHttpEntity {
849        protected final Object object;
850
851        protected HttpEntityImpl(Object object) {
852            super();
853
854            this.object = requireNonNull(object, "object");
855
856            setChunked(false);
857        }
858
859        @Override
860        public boolean isRepeatable() { return true; }
861
862        @Override
863        public long getContentLength() { return -1; }
864
865        @Override
866        public InputStream getContent() throws IOException,
867                                               IllegalStateException {
868            ByteArrayDataSource ds = new ByteArrayDataSource(null, null);
869
870            try (OutputStream out = ds.getOutputStream()) {
871                writeTo(out);
872            }
873
874            return ds.getInputStream();
875        }
876
877        @Override
878        public boolean isStreaming() { return false; }
879    }
880
881    private class JSONHttpEntity extends HttpEntityImpl {
882        public JSONHttpEntity(Object object) {
883            super(object);
884
885            setContentType(ContentType.APPLICATION_JSON
886                           .withCharset(client.getCharset())
887                           .toString());
888        }
889
890        @Override
891        public void writeTo(OutputStream out) throws IOException {
892            client.getObjectMapper().writeValue(out, object);
893        }
894    }
895}