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}