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