001package ball.http.ant.taskdefs;
002/*-
003 * ##########################################################################
004 * Web API Client (HTTP) Utilities
005 * $Id: HTTPTask.java 5285 2020-02-05 04:23:21Z ball $
006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-http/trunk/src/main/java/ball/http/ant/taskdefs/HTTPTask.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.ReaderWriterDataSource;
024import ball.swing.table.MapTableModel;
025import ball.util.PropertiesImpl;
026import ball.util.ant.taskdefs.AnnotatedAntTask;
027import ball.util.ant.taskdefs.AntTask;
028import ball.util.ant.taskdefs.ClasspathDelegateAntTask;
029import ball.util.ant.taskdefs.ConfigurableAntTask;
030import ball.util.ant.types.StringAttributeType;
031import java.io.IOException;
032import java.io.OutputStream;
033import java.net.URISyntaxException;
034import java.nio.charset.Charset;
035import java.util.ArrayList;
036import java.util.List;
037import java.util.Map;
038import lombok.Getter;
039import lombok.NoArgsConstructor;
040import lombok.Setter;
041import lombok.ToString;
042import lombok.experimental.Accessors;
043import org.apache.commons.beanutils.BeanMap;
044import org.apache.http.Header;
045import org.apache.http.HttpEntity;
046import org.apache.http.HttpEntityEnclosingRequest;
047import org.apache.http.HttpMessage;
048import org.apache.http.HttpRequest;
049import org.apache.http.HttpRequestInterceptor;
050import org.apache.http.HttpResponse;
051import org.apache.http.HttpResponseInterceptor;
052import org.apache.http.NameValuePair;
053import org.apache.http.client.methods.HttpDelete;
054import org.apache.http.client.methods.HttpGet;
055import org.apache.http.client.methods.HttpHead;
056import org.apache.http.client.methods.HttpOptions;
057import org.apache.http.client.methods.HttpPatch;
058import org.apache.http.client.methods.HttpPost;
059import org.apache.http.client.methods.HttpPut;
060import org.apache.http.client.methods.HttpRequestBase;
061import org.apache.http.client.methods.HttpUriRequest;
062import org.apache.http.client.utils.URIBuilder;
063import org.apache.http.entity.BufferedHttpEntity;
064import org.apache.http.entity.StringEntity;
065import org.apache.http.impl.client.CloseableHttpClient;
066import org.apache.http.impl.client.HttpClientBuilder;
067import org.apache.http.protocol.HttpContext;
068import org.apache.tools.ant.BuildException;
069import org.apache.tools.ant.Task;
070import org.apache.tools.ant.util.ClasspathUtils;
071
072import static ball.activation.ReaderWriterDataSource.CONTENT_TYPE;
073import static lombok.AccessLevel.PROTECTED;
074import static org.apache.commons.lang3.StringUtils.EMPTY;
075import static org.apache.commons.lang3.StringUtils.isEmpty;
076import static org.apache.tools.ant.Project.toBoolean;
077
078/**
079 * Abstract {@link.uri http://ant.apache.org/ Ant} base {@link Task} for web
080 * API client tasks.
081 *
082 * {@ant.task}
083 *
084 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
085 * @version $Revision: 5285 $
086 */
087@NoArgsConstructor(access = PROTECTED)
088public abstract class HTTPTask extends Task
089                               implements AnnotatedAntTask,
090                                          ClasspathDelegateAntTask,
091                                          ConfigurableAntTask,
092                                          HttpRequestInterceptor,
093                                          HttpResponseInterceptor {
094    private static final String DOT = ".";
095
096    private final HttpClientBuilder builder =
097        HttpClientBuilder.create()
098        .addInterceptorLast((HttpRequestInterceptor) this)
099        .addInterceptorLast((HttpResponseInterceptor) this);
100
101    @Getter @Setter @Accessors(chain = true, fluent = true)
102    private ClasspathUtils.Delegate delegate = null;
103    @Getter @Setter
104    private boolean buffer = false;
105
106    @Override
107    public void init() throws BuildException {
108        super.init();
109        ClasspathDelegateAntTask.super.init();
110        ConfigurableAntTask.super.init();
111    }
112
113    @Override
114    public void execute() throws BuildException {
115        super.execute();
116        AnnotatedAntTask.super.execute();
117    }
118
119    /**
120     * Method to allow subclasses to configure the
121     * {@link HttpClientBuilder}.
122     *
123     * @return  The {@link HttpClientBuilder}.
124     */
125    protected HttpClientBuilder builder() { return builder; }
126
127    @Override
128    public void process(HttpRequest request,
129                        HttpContext context) throws IOException {
130        if (request instanceof HttpEntityEnclosingRequest) {
131            HttpEntity entity =
132                ((HttpEntityEnclosingRequest) request).getEntity();
133
134            if (entity != null) {
135                if (! entity.isRepeatable()) {
136                    if (isBuffer()) {
137                        ((HttpEntityEnclosingRequest) request)
138                            .setEntity(new BufferedHttpEntity(entity));
139                    }
140                }
141            }
142        }
143
144        log();
145        log(context);
146        log();
147        log(request);
148    }
149
150    @Override
151    public void process(HttpResponse response,
152                        HttpContext context) throws IOException {
153        HttpEntity entity = response.getEntity();
154
155        if (entity != null) {
156            if (! entity.isRepeatable()) {
157                if (isBuffer()) {
158                    response.setEntity(new BufferedHttpEntity(entity));
159                }
160            }
161        }
162
163        log();
164        log(context);
165        log();
166        log(response);
167    }
168
169    /**
170     * See {@link #log(String)}.
171     *
172     * @param   context         The {@link HttpContext} to log.
173     */
174    protected void log(HttpContext context) {
175        log(new MapTableModel(new BeanMap(context)));
176    }
177
178    /**
179     * See {@link #log(String)}.
180     *
181     * @param   message         The {@link HttpMessage} to log.
182     */
183    protected void log(HttpMessage message) {
184        if (message instanceof HttpRequest) {
185            log(String.valueOf(((HttpRequest) message).getRequestLine()));
186        }
187
188        if (message instanceof HttpResponse) {
189            log(String.valueOf(((HttpResponse) message).getStatusLine()));
190        }
191
192        for (Header header : message.getAllHeaders()) {
193            log(String.valueOf(header));
194        }
195
196        log(getContentType(message), getHttpEntity(message));
197    }
198
199    private String getContentType(HttpMessage message) {
200        return (message.containsHeader(CONTENT_TYPE)
201                    ? message.getFirstHeader(CONTENT_TYPE).getValue()
202                    : null);
203    }
204
205    private HttpEntity getHttpEntity(HttpMessage message) {
206        HttpEntity entity = null;
207
208        if (entity == null) {
209            if (message instanceof HttpEntityEnclosingRequest) {
210                entity = ((HttpEntityEnclosingRequest) message).getEntity();
211            }
212        }
213
214        if (entity == null) {
215            if (message instanceof HttpResponse) {
216                entity = ((HttpResponse) message).getEntity();
217            }
218        }
219
220        return entity;
221    }
222
223    /**
224     * See {@link #log(String)}.
225     *
226     * @param   type            The entity {@code Content-Type} (if
227     *                          specified).
228     * @param   entity          The {@link HttpEntity} to log.
229     */
230    protected void log(String type, HttpEntity entity) {
231        if (entity != null) {
232            if (entity.isRepeatable()) {
233                ReaderWriterDataSource ds =
234                    new ReaderWriterDataSource(null, type);
235
236                try (OutputStream out = ds.getOutputStream()) {
237                    entity.writeTo(out);
238                    out.flush();
239
240                    String string = ds.toString();
241
242                    if (! isEmpty(string)) {
243                        log();
244                        log(string);
245                    }
246                } catch (IOException exception) {
247                }
248            } else {
249                log(String.valueOf(entity));
250            }
251        }
252    }
253
254    /**
255     * Abstract {@link.uri http://ant.apache.org/ Ant} base
256     * {@link org.apache.tools.ant.Task} for DELETE, GET, POST, and PUT
257     * operations.
258     *
259     * {@ant.task}
260     */
261    @NoArgsConstructor(access = PROTECTED)
262    protected static abstract class Request extends HTTPTask {
263        private PropertiesImpl properties = null;
264        private URIBuilder builder = new URIBuilder();
265        private final List<NameValuePairImpl> headers = new ArrayList<>();
266
267        @Getter @Setter
268        private String content = null;
269
270        public void setURI(String string) throws URISyntaxException {
271            builder = new URIBuilder(string);
272        }
273
274        public void setCharset(String string) {
275            builder.setCharset(Charset.forName(string));
276        }
277
278        public void setFragment(String string) { builder.setFragment(string); }
279        public void setHost(String string) { builder.setHost(string); }
280        public void setPath(String string) { builder.setPath(string); }
281        public void setPort(Integer integer) { builder.setPort(integer); }
282        public void setQuery(String string) { builder.setCustomQuery(string); }
283        public void setScheme(String string) { builder.setScheme(string); }
284        public void setUserInfo(String string) { builder.setUserInfo(string); }
285
286        public void addConfiguredParameter(NameValuePairImpl parameter) {
287            builder.addParameter(parameter.getName(), parameter.getValue());
288        }
289
290        public void addConfiguredHeader(NameValuePairImpl header) {
291            headers.add(header);
292        }
293
294        public void addText(String text) {
295            setContent((isEmpty(getContent()) ? EMPTY : getContent()) + text);
296        }
297
298        @Override
299        public void init() throws BuildException {
300            super.init();
301
302            String method = getClass().getSimpleName().toUpperCase();
303
304            properties =
305                getPrefixedProperties(method + DOT,
306                                      getProject().getProperties());
307
308            try {
309                if (properties.containsKey("uri")) {
310                    builder = new URIBuilder(properties.getProperty("uri"));
311                }
312
313                properties.configure(builder);
314
315                for (Map.Entry<?,?> entry :
316                         getPrefixedProperties("parameter" + DOT, properties)
317                         .entrySet()) {
318                    builder.addParameter(entry.getKey().toString(),
319                                         entry.getValue().toString());
320                }
321            } catch (BuildException exception) {
322                throw exception;
323            } catch (Throwable throwable) {
324                throwable.printStackTrace();
325                throw new BuildException(throwable);
326            }
327        }
328
329        private PropertiesImpl getPrefixedProperties(String prefix,
330                                                     Map<?,?> map) {
331            PropertiesImpl properties = new PropertiesImpl();
332
333            for (Map.Entry<?,?> entry : map.entrySet()) {
334                Object key = entry.getKey();
335                String string = (key != null) ? key.toString() : null;
336
337                if ((! isEmpty(string)) && string.startsWith(prefix)) {
338                    properties.put(string.substring(prefix.length()),
339                                   entry.getValue());
340                }
341            }
342
343            return properties;
344        }
345
346        /**
347         * Method to construct the {@link HTTPTask}-specific
348         * {@link HttpUriRequest}.
349         *
350         * @return      The {@link HttpUriRequest}.
351         */
352        protected abstract HttpUriRequest request();
353
354        /**
355         * Method to configure the {@link HTTPTask} {@link HttpUriRequest}.
356         * See {@link #execute()} and {@link #request()}.
357         *
358         * @param       request         The {@link HttpUriRequest}.
359         *
360         * @throws      Exception       If an exception is encountered.
361         */
362        protected void configure(HttpUriRequest request) throws Exception {
363            ((HttpRequestBase) request).setURI(builder.build());
364
365            addHeaders(request,
366                       getPrefixedProperties("header" + DOT, properties)
367                       .entrySet());
368            addHeaders(request, headers);
369
370            if (! isEmpty(getContent())) {
371                setEntity(request, getContent());
372            } else if (! isEmpty(properties.getProperty("content"))) {
373                setEntity(request, properties.getProperty("content"));
374            }
375        }
376
377        private void addHeaders(HttpRequest request,
378                                Iterable<? extends Map.Entry<?,?>> iterable) {
379            for (Map.Entry<?,?> entry : iterable) {
380                request.addHeader(entry.getKey().toString(),
381                                  entry.getValue().toString());
382            }
383        }
384
385        private void setEntity(HttpUriRequest request,
386                               String content) throws Exception {
387            if (! isEmpty(content)) {
388                ((HttpEntityEnclosingRequest) request)
389                    .setEntity(new StringEntity(content));
390            }
391        }
392
393        @Override
394        public void execute() throws BuildException {
395            super.execute();
396
397            try (CloseableHttpClient client = builder().build()) {
398                HttpUriRequest request = request();
399
400                configure(request);
401
402                HttpResponse response = client.execute(request);
403            } catch (BuildException exception) {
404                throw exception;
405            } catch (Throwable throwable) {
406                throwable.printStackTrace();
407                throw new BuildException(throwable);
408            }
409        }
410    }
411
412    /**
413     * {@link.uri http://ant.apache.org/ Ant}
414     * {@link org.apache.tools.ant.Task} to DELETE.
415     *
416     * {@ant.task}
417     */
418    @AntTask("http-delete")
419    @NoArgsConstructor @ToString
420    public static class Delete extends Request {
421        @Override
422        protected HttpUriRequest request() { return new HttpDelete(); }
423    }
424
425    /**
426     * {@link.uri http://ant.apache.org/ Ant}
427     * {@link org.apache.tools.ant.Task} to GET.
428     *
429     * {@ant.task}
430     */
431    @AntTask("http-get")
432    @NoArgsConstructor @ToString
433    public static class Get extends Request {
434        @Override
435        protected HttpUriRequest request() { return new HttpGet(); }
436    }
437
438    /**
439     * {@link.uri http://ant.apache.org/ Ant}
440     * {@link org.apache.tools.ant.Task} to HEAD.
441     *
442     * {@ant.task}
443     */
444    @AntTask("http-head")
445    @NoArgsConstructor @ToString
446    public static class Head extends Request {
447        @Override
448        protected HttpUriRequest request() { return new HttpHead(); }
449    }
450
451    /**
452     * {@link.uri http://ant.apache.org/ Ant}
453     * {@link org.apache.tools.ant.Task} to OPTIONS.
454     *
455     * {@ant.task}
456     */
457    @AntTask("http-options")
458    @NoArgsConstructor @ToString
459    public static class Options extends Request {
460        @Override
461        protected HttpUriRequest request() { return new HttpOptions(); }
462    }
463
464    /**
465     * {@link.uri http://ant.apache.org/ Ant}
466     * {@link org.apache.tools.ant.Task} to PATCH.
467     *
468     * {@ant.task}
469     */
470    @AntTask("http-patch")
471    @NoArgsConstructor @ToString
472    public static class Patch extends Request {
473        @Override
474        protected HttpUriRequest request() { return new HttpPatch(); }
475    }
476
477    /**
478     * {@link.uri http://ant.apache.org/ Ant}
479     * {@link org.apache.tools.ant.Task} to POST.
480     *
481     * {@ant.task}
482     */
483    @AntTask("http-post")
484    @NoArgsConstructor @ToString
485    public static class Post extends Request {
486        @Override
487        protected HttpUriRequest request() { return new HttpPost(); }
488    }
489
490    /**
491     * {@link.uri http://ant.apache.org/ Ant}
492     * {@link org.apache.tools.ant.Task} to PUT.
493     *
494     * {@ant.task}
495     */
496    @AntTask("http-put")
497    @NoArgsConstructor @ToString
498    public static class Put extends Request {
499        @Override
500        protected HttpUriRequest request() { return new HttpPut(); }
501    }
502
503    /**
504     * {@link StringAttributeType} implementation that includes
505     * {@link NameValuePair}.
506     */
507    @NoArgsConstructor @ToString
508    public static class NameValuePairImpl extends StringAttributeType
509                                          implements NameValuePair {
510    }
511}