You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ant.apache.org by mb...@apache.org on 2022/03/31 14:20:43 UTC

[ant-antlibs-s3] branch main updated (8a87873 -> a6252af)

This is an automated email from the ASF dual-hosted git repository.

mbenson pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-s3.git.


    from 8a87873  ProjectUtils to interface
     new 53caf51  enhancements
     new 2758938  upgrade AWS SDK
     new b9846a7  .gitignore
     new 233cd38  exclude prefixes where possible
     new 3178af3  publish-local
     new da99c88  cleanup + javadoc
     new a6252af  total overhaul of SDK building

The 7 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .gitignore                                         |   3 +
 build.properties                                   |   2 +-
 build.xml                                          |  24 +
 ivy.xml                                            |  10 +-
 src/main/org/apache/ant/s3/Builder.java            | 208 ---------
 src/main/org/apache/ant/s3/Client.java             |  79 +---
 src/main/org/apache/ant/s3/CompareSelect.java      |  27 +-
 src/main/org/apache/ant/s3/CopyResources.java      |  48 +-
 src/main/org/apache/ant/s3/Credentials.java        | 122 -----
 src/main/org/apache/ant/s3/Delete.java             |  12 +-
 src/main/org/apache/ant/s3/Exceptions.java         |   2 +-
 src/main/org/apache/ant/s3/HttpConfiguration.java  |  57 ---
 src/main/org/apache/ant/s3/InlineProperties.java   |  19 +-
 src/main/org/apache/ant/s3/LoggingTask.java        |  37 +-
 src/main/org/apache/ant/s3/ObjectResource.java     | 100 +++-
 src/main/org/apache/ant/s3/ObjectResources.java    |  25 +-
 src/main/org/apache/ant/s3/ProjectUtils.java       |  22 +-
 src/main/org/apache/ant/s3/Put.java                |  19 +-
 src/main/org/apache/ant/s3/S3DataType.java         |  15 +-
 src/main/org/apache/ant/s3/S3Finder.java           |  11 +-
 src/main/org/apache/ant/s3/StringConversions.java  | 176 -------
 .../AwsStringConversionsProvider.java}             |  41 +-
 .../org/apache/ant/s3/build/BuildableSupplier.java | 133 ++++++
 src/main/org/apache/ant/s3/build/Buildables.java   |  94 ++++
 src/main/org/apache/ant/s3/build/Builder.java      | 266 +++++++++++
 src/main/org/apache/ant/s3/build/ClassFinder.java  | 138 ++++++
 .../apache/ant/s3/build/ConfigurableSupplier.java  |  77 ++++
 .../ant/s3/build/ConfigurableSupplierFactory.java  |  76 +++
 .../ConfiguringSupplier.java}                      |  33 +-
 .../apache/ant/s3/build/ConfiguringSuppliers.java  |  96 ++++
 .../s3/build/DefaultStringConversionsProvider.java | 116 +++++
 .../org/apache/ant/s3/build/MetaBuilderByType.java | 140 ++++++
 .../org/apache/ant/s3/build/MethodSignature.java   | 126 +++++
 .../ant/s3/build/RootConfiguringSupplier.java      | 116 +++++
 .../org/apache/ant/s3/build/StringConversions.java | 248 ++++++++++
 .../s3/build/spi/ConfiguringSuppliersProvider.java |  76 +++
 .../spi/DefaultProvider.java}                      |  33 +-
 .../s3/build/spi/IntrospectingProviderBase.java    | 183 ++++++++
 .../org/apache/ant/s3/build/spi/Providers.java     |  64 +++
 .../s3/build/spi/StringConversionsProvider.java    |  94 ++++
 .../CredentialsConfiguringSuppliersProvider.java   | 513 +++++++++++++++++++++
 .../http/ClientConfiguringSuppliersProvider.java   |  82 ++++
 src/main/org/apache/ant/s3/strings/ClassNames.java | 234 ++++++++++
 .../org/apache/ant/s3/strings/PackageNames.java    | 214 +++++++++
 src/main/org/apache/ant/s3/strings/Strings.java    | 128 +++++
 src/tests/antunit/s3-test-base.xml                 |   9 +-
 .../apache/ant/s3/build/StringConversionsTest.java | 156 +++++++
 .../org/apache/ant/s3/strings/ClassNamesTest.java  | 345 ++++++++++++++
 .../apache/ant/s3/strings/PackageNamesTest.java    | 286 ++++++++++++
 .../org/apache/ant/s3/strings/StringsTest.java     | 114 +++++
 50 files changed, 4462 insertions(+), 787 deletions(-)
 create mode 100644 .gitignore
 delete mode 100644 src/main/org/apache/ant/s3/Builder.java
 delete mode 100644 src/main/org/apache/ant/s3/Credentials.java
 delete mode 100644 src/main/org/apache/ant/s3/HttpConfiguration.java
 delete mode 100644 src/main/org/apache/ant/s3/StringConversions.java
 copy src/main/org/apache/ant/s3/{Counter.java => build/AwsStringConversionsProvider.java} (50%)
 create mode 100644 src/main/org/apache/ant/s3/build/BuildableSupplier.java
 create mode 100644 src/main/org/apache/ant/s3/build/Buildables.java
 create mode 100644 src/main/org/apache/ant/s3/build/Builder.java
 create mode 100644 src/main/org/apache/ant/s3/build/ClassFinder.java
 create mode 100644 src/main/org/apache/ant/s3/build/ConfigurableSupplier.java
 create mode 100644 src/main/org/apache/ant/s3/build/ConfigurableSupplierFactory.java
 copy src/main/org/apache/ant/s3/{Precision.java => build/ConfiguringSupplier.java} (59%)
 create mode 100644 src/main/org/apache/ant/s3/build/ConfiguringSuppliers.java
 create mode 100644 src/main/org/apache/ant/s3/build/DefaultStringConversionsProvider.java
 create mode 100644 src/main/org/apache/ant/s3/build/MetaBuilderByType.java
 create mode 100644 src/main/org/apache/ant/s3/build/MethodSignature.java
 create mode 100644 src/main/org/apache/ant/s3/build/RootConfiguringSupplier.java
 create mode 100644 src/main/org/apache/ant/s3/build/StringConversions.java
 create mode 100644 src/main/org/apache/ant/s3/build/spi/ConfiguringSuppliersProvider.java
 copy src/main/org/apache/ant/s3/{Precision.java => build/spi/DefaultProvider.java} (57%)
 create mode 100644 src/main/org/apache/ant/s3/build/spi/IntrospectingProviderBase.java
 create mode 100644 src/main/org/apache/ant/s3/build/spi/Providers.java
 create mode 100644 src/main/org/apache/ant/s3/build/spi/StringConversionsProvider.java
 create mode 100644 src/main/org/apache/ant/s3/credentials/CredentialsConfiguringSuppliersProvider.java
 create mode 100644 src/main/org/apache/ant/s3/http/ClientConfiguringSuppliersProvider.java
 create mode 100644 src/main/org/apache/ant/s3/strings/ClassNames.java
 create mode 100644 src/main/org/apache/ant/s3/strings/PackageNames.java
 create mode 100644 src/main/org/apache/ant/s3/strings/Strings.java
 create mode 100644 src/tests/junit/org/apache/ant/s3/build/StringConversionsTest.java
 create mode 100644 src/tests/junit/org/apache/ant/s3/strings/ClassNamesTest.java
 create mode 100644 src/tests/junit/org/apache/ant/s3/strings/PackageNamesTest.java
 create mode 100644 src/tests/junit/org/apache/ant/s3/strings/StringsTest.java

[ant-antlibs-s3] 06/07: cleanup + javadoc

Posted by mb...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

mbenson pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-s3.git

commit da99c88a9097e79121cf7c128a6fa155264ab60d
Author: Matt Benson <mb...@apache.org>
AuthorDate: Thu Mar 31 09:04:40 2022 -0500

    cleanup + javadoc
---
 src/main/org/apache/ant/s3/CompareSelect.java    |  27 +++++-
 src/main/org/apache/ant/s3/CopyResources.java    |  48 ++++++++---
 src/main/org/apache/ant/s3/Delete.java           |  12 ++-
 src/main/org/apache/ant/s3/InlineProperties.java |  10 +--
 src/main/org/apache/ant/s3/LoggingTask.java      |  37 +++++----
 src/main/org/apache/ant/s3/ObjectResource.java   | 100 ++++++++++++++++++++---
 src/main/org/apache/ant/s3/ObjectResources.java  |  25 ++++--
 src/main/org/apache/ant/s3/ProjectUtils.java     |  20 ++---
 src/main/org/apache/ant/s3/Put.java              |  19 ++++-
 src/main/org/apache/ant/s3/S3DataType.java       |  15 ++--
 10 files changed, 234 insertions(+), 79 deletions(-)

diff --git a/src/main/org/apache/ant/s3/CompareSelect.java b/src/main/org/apache/ant/s3/CompareSelect.java
index 4cb4fd4..d3521ea 100644
--- a/src/main/org/apache/ant/s3/CompareSelect.java
+++ b/src/main/org/apache/ant/s3/CompareSelect.java
@@ -102,6 +102,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Create a new {@link CompareSelect.ForStringAttribute}.
          *
          * @param project
+         *            Ant {@link Project}
          */
         protected ForStringAttribute(final Project project) {
             super(project);
@@ -112,6 +113,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Add nested text by which the value to compare is set.
          *
          * @param text
+         *            to add
          */
         public void addText(final String text) {
             Optional.ofNullable(StringUtils.trimToNull(text)).map(getProject()::replaceProperties).map(String::trim)
@@ -122,7 +124,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
         }
 
         /**
-         * Get "match as" strategy (default {@link MatchAs#LITERAL}.
+         * Get "match as" strategy (default {@link MatchAs#literal}.
          *
          * @return {@link MatchAs}
          */
@@ -134,6 +136,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Set "match as" strategy.
          *
          * @param matchAs
+         *            strategy
          */
         public void setMatchAs(final MatchAs matchAs) {
             Exceptions.raiseIf(matchAs == null, buildException(), "@matchas may not be null");
@@ -158,6 +161,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Set whether matching should be performed in a case-sensitive manner.
          *
          * @param caseSensitive
+         *            flag
          */
         public void setCaseSensitive(final boolean caseSensitive) {
             if (caseSensitive != this.caseSensitive) {
@@ -179,6 +183,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * {@link ObjectResource}.
          *
          * @param obj
+         *            owning value
          * @return {@link String}
          */
         protected abstract String extractValueFrom(ObjectResource obj);
@@ -217,6 +222,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Create a new {@link Bucket} selector.
          *
          * @param project
+         *            Ant {@link Project}
          */
         public Bucket(final Project project) {
             super(project);
@@ -240,6 +246,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Create a new {@link Key} selector.
          *
          * @param project
+         *            Ant {@link Project}
          */
         public Key(final Project project) {
             super(project);
@@ -263,6 +270,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Create a new {@link ContentType} selector.
          *
          * @param project
+         *            Ant {@link Project}
          */
         public ContentType(final Project project) {
             super(project);
@@ -287,6 +295,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Create a new {@link Meta} selector.
          *
          * @param project
+         *            Ant {@link Project}
          */
         public Meta(final Project project) {
             super(project);
@@ -305,6 +314,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Set the user metadata key to match on.
          *
          * @param key
+         *            to match
          */
         public void setKey(final String key) {
             this.key = key;
@@ -329,6 +339,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Create a new {@link Tag} selector.
          *
          * @param project
+         *            Ant {@link Project}
          */
         public Tag(final Project project) {
             super(project);
@@ -347,6 +358,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Set the tag key to match on.
          *
          * @param key
+         *            to match
          */
         public void setKey(final String key) {
             this.key = key;
@@ -370,11 +382,15 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Create a new {@link VersionId} selector.
          * 
          * @param project
+         *            Ant {@link Project}
          */
         public VersionId(Project project) {
             super(project);
         }
 
+        /**
+         * {@inheritDoc}
+         */
         @Override
         protected String extractValueFrom(ObjectResource obj) {
             return obj.getVersionId();
@@ -392,6 +408,9 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Create a new {@link CompareSelect.ForBooleanAttribute}.
          * 
          * @param project
+         *            Ant {@link Project}
+         * @param test
+         *            to determine truth
          */
         protected ForBooleanAttribute(Project project, Predicate<ObjectResource> test) {
             super(project);
@@ -425,6 +444,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Create a new {@link DeleteMarker}.
          * 
          * @param project
+         *            Ant {@link Project}
          */
         public DeleteMarker(Project project) {
             super(project, ObjectResource::isDeleteMarker);
@@ -440,6 +460,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Create a new {@link Latest}.
          * 
          * @param project
+         *            Ant {@link Project}
          */
         public Latest(Project project) {
             super(project, ObjectResource::isLatest);
@@ -458,6 +479,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Create a new {@link ByPrecision}.
          * 
          * @param project
+         *            Ant {@link Project}
          */
         public ByPrecision(Project project) {
             super(project);
@@ -476,7 +498,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
          * Set the precision.
          * 
          * @param precision
-         *            Precision
+         *            {@link Precision}
          */
         public void setPrecision(Precision precision) {
             this.precision = precision;
@@ -512,6 +534,7 @@ public abstract class CompareSelect extends ResourceComparator implements Resour
      * Create a {@link CompareSelect} instance.
      * 
      * @param project
+     *            Ant {@link Project}
      */
     protected CompareSelect(Project project) {
         setProject(project);
diff --git a/src/main/org/apache/ant/s3/CopyResources.java b/src/main/org/apache/ant/s3/CopyResources.java
index 43d9098..ea68c29 100644
--- a/src/main/org/apache/ant/s3/CopyResources.java
+++ b/src/main/org/apache/ant/s3/CopyResources.java
@@ -54,6 +54,16 @@ public abstract class CopyResources extends LoggingTask {
     private boolean preserveLastModified;
 
     /**
+     * Create a new {@link CopyResources} instance.
+     *
+     * @param project
+     *            Ant {@link Project}
+     */
+    protected CopyResources(Project project) {
+        super(project);
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
@@ -105,8 +115,9 @@ public abstract class CopyResources extends LoggingTask {
      * Add a nested source {@link ResourceCollection}.
      *
      * @param sources
+     *            to add
      */
-    public void add(final ResourceCollection sources) {
+    public void add(ResourceCollection sources) {
         this.sources.add(sources);
     }
 
@@ -131,7 +142,7 @@ public abstract class CopyResources extends LoggingTask {
      * @param fileNameMapper
      *            the {@link FileNameMapper} to add
      */
-    public void add(final FileNameMapper fileNameMapper) {
+    public void add(FileNameMapper fileNameMapper) {
         createMapper().add(fileNameMapper);
     }
 
@@ -148,8 +159,9 @@ public abstract class CopyResources extends LoggingTask {
      * Set the input encoding.
      *
      * @param inputEncoding
+     *            to set
      */
-    public void setInputEncoding(final String inputEncoding) {
+    public void setInputEncoding(String inputEncoding) {
         this.inputEncoding = inputEncoding;
     }
 
@@ -166,8 +178,9 @@ public abstract class CopyResources extends LoggingTask {
      * Set the output encoding.
      *
      * @param outputEncoding
+     *            to set
      */
-    public void setOutputEncoding(final String outputEncoding) {
+    public void setOutputEncoding(String outputEncoding) {
         this.outputEncoding = outputEncoding;
     }
 
@@ -184,8 +197,9 @@ public abstract class CopyResources extends LoggingTask {
      * Set whether global Ant filters should be applied to copy operations.
      *
      * @param filtering
+     *            flag
      */
-    public void setFiltering(final boolean filtering) {
+    public void setFiltering(boolean filtering) {
         this.filtering = filtering;
     }
 
@@ -224,8 +238,9 @@ public abstract class CopyResources extends LoggingTask {
      * Set whether up-to-date target {@link Resource}s should be overwritten.
      *
      * @param overwrite
+     *            flag
      */
-    public void setOverwrite(final boolean overwrite) {
+    public void setOverwrite(boolean overwrite) {
         this.overwrite = overwrite;
     }
 
@@ -248,15 +263,16 @@ public abstract class CopyResources extends LoggingTask {
      * be copied to.
      *
      * @param enableMultipleMappings
+     *            flag
      */
-    public void setEnableMultipleMappings(final boolean enableMultipleMappings) {
+    public void setEnableMultipleMappings(boolean enableMultipleMappings) {
         this.enableMultipleMappings = enableMultipleMappings;
     }
 
     /**
      * Learn whether target {@link Resource} content should be appended.
      *
-     * @return
+     * @return {@code boolean}
      */
     public boolean isAppend() {
         return append;
@@ -266,8 +282,9 @@ public abstract class CopyResources extends LoggingTask {
      * Set whether target {@link Resource} content should be appended.
      *
      * @param append
+     *            flag
      */
-    public void setAppend(final boolean append) {
+    public void setAppend(boolean append) {
         this.append = append;
     }
 
@@ -285,8 +302,9 @@ public abstract class CopyResources extends LoggingTask {
      * Set whether to preserve last modified time on target {@link Resource}s.
      *
      * @param preserveLastModified
+     *            flag
      */
-    public void setPreserveLastModified(final boolean preserveLastModified) {
+    public void setPreserveLastModified(boolean preserveLastModified) {
         this.preserveLastModified = preserveLastModified;
     }
 
@@ -302,13 +320,17 @@ public abstract class CopyResources extends LoggingTask {
      * {@link FilterSetCollection} and {@code filterChains}.
      *
      * @param source
+     *            {@link Resource}
      * @param dest
+     *            {@link Resource}
      * @param filters
+     *            {@link FilterSetCollection}
      * @param filterChains
-     * @throws IOException
+     *            {@link Vector} of {@link FilterChain}
+     * @throws IOException on error
      */
-    protected void copyResource(final Resource source, final Resource dest, final FilterSetCollection filters,
-        final Vector<FilterChain> filterChains) throws IOException {
+    protected void copyResource(Resource source, Resource dest, FilterSetCollection filters,
+        Vector<FilterChain> filterChains) throws IOException {
         ResourceUtils.copyResource(source, dest, filters, filterChains, isOverwrite(), isPreserveLastModified(),
             isAppend(), getInputEncoding(), getOutputEncoding(), getProject());
     }
diff --git a/src/main/org/apache/ant/s3/Delete.java b/src/main/org/apache/ant/s3/Delete.java
index 0700d65..a71ec98 100644
--- a/src/main/org/apache/ant/s3/Delete.java
+++ b/src/main/org/apache/ant/s3/Delete.java
@@ -53,17 +53,11 @@ public class Delete extends LoggingTask {
     private int blockSize = DEFAULT_BLOCK_SIZE;
 
     /**
-     * Create a new {@link Delete} task instance.
-     */
-    public Delete() {
-        super();
-    }
-
-    /**
      * Create a new {@link Delete} task instance bound to the specified
      * {@link Project}.
      * 
      * @param project
+     *            Ant {@link Project}
      */
     public Delete(Project project) {
         super(project);
@@ -98,6 +92,7 @@ public class Delete extends LoggingTask {
      * Add a configured {@link Client}.
      *
      * @param s3
+     *            {@link Client}
      */
     public void addConfigured(final Client s3) {
         if (this.s3 != null) {
@@ -110,6 +105,7 @@ public class Delete extends LoggingTask {
      * Set the {@link Client} by reference.
      *
      * @param refid
+     *            of {@link Client}
      */
     public void setClientRefid(final String refid) {
         Objects.requireNonNull(StringUtils.trimToNull(refid), "@clientrefid must not be null/empty/blank");
@@ -121,6 +117,7 @@ public class Delete extends LoggingTask {
      * Add a nested {@link ResourceCollection}.
      * 
      * @param coll
+     *            {@link ResourceCollection}
      */
     public synchronized void addConfigured(ResourceCollection coll) {
         Exceptions.raiseIf(coll == null, IllegalArgumentException::new, "null %s",
@@ -140,6 +137,7 @@ public class Delete extends LoggingTask {
      * Add by reference a {@link ResourceCollection} to delete.
      * 
      * @param refid
+     *            of {@link ResourceCollection}
      */
     public void setRefid(Reference refid) {
         addConfigured(refid.<ResourceCollection> getReferencedObject(getProject()));
diff --git a/src/main/org/apache/ant/s3/InlineProperties.java b/src/main/org/apache/ant/s3/InlineProperties.java
index 7aff31d..22f4f89 100644
--- a/src/main/org/apache/ant/s3/InlineProperties.java
+++ b/src/main/org/apache/ant/s3/InlineProperties.java
@@ -40,9 +40,9 @@ public class InlineProperties extends S3DataType implements DynamicElementNS {
     /**
      * Create a new {@link InlineProperties} instance.
      * 
-     * @param project
+     * @param project Ant {@link Project}
      */
-    public InlineProperties(final Project project) {
+    public InlineProperties(Project project) {
         super(project);
     }
 
@@ -52,7 +52,7 @@ public class InlineProperties extends S3DataType implements DynamicElementNS {
     public final class InlineProperty {
         private final String name;
 
-        private InlineProperty(final String name) {
+        private InlineProperty(String name) {
             this.name = name;
         }
 
@@ -62,7 +62,7 @@ public class InlineProperties extends S3DataType implements DynamicElementNS {
          * @param text
          *            to add
          */
-        public void addText(final String text) {
+        public void addText(String text) {
             final String value;
             if (properties.containsKey(name)) {
                 value = Stream.of(properties.getProperty(name), text).filter(Objects::nonNull)
@@ -91,7 +91,7 @@ public class InlineProperties extends S3DataType implements DynamicElementNS {
      * @return InlineProperty
      */
     @Override
-    public InlineProperty createDynamicElement(final String uri, final String localName, final String qName) {
+    public InlineProperty createDynamicElement(String uri, String localName, String qName) {
         return new InlineProperty(localName);
     }
 }
\ No newline at end of file
diff --git a/src/main/org/apache/ant/s3/LoggingTask.java b/src/main/org/apache/ant/s3/LoggingTask.java
index c69203a..2ea09d2 100644
--- a/src/main/org/apache/ant/s3/LoggingTask.java
+++ b/src/main/org/apache/ant/s3/LoggingTask.java
@@ -29,20 +29,13 @@ abstract class LoggingTask extends Task implements ProjectUtils {
     private int verbosity = Project.MSG_VERBOSE;
 
     /**
-     * Create a new {@link LoggingTask} instance.
-     */
-    protected LoggingTask() {
-        super();
-    }
-
-    /**
      * Create a new {@link LoggingTask} instance bound to the specified
      * {@link Project}.
      * 
      * @param project
+     *            Ant {@link Project}
      */
     protected LoggingTask(Project project) {
-        this();
         setProject(project);
     }
 
@@ -59,8 +52,9 @@ abstract class LoggingTask extends Task implements ProjectUtils {
      * Set whether this {@link Task} should operate with increased log output.
      *
      * @param verbose
+     *            flag
      */
-    public void setVerbose(final boolean verbose) {
+    public void setVerbose(boolean verbose) {
         verbosity = verbose ? Project.MSG_INFO : Project.MSG_VERBOSE;
     }
 
@@ -69,10 +63,12 @@ abstract class LoggingTask extends Task implements ProjectUtils {
      * {@link #isVerbose()}.
      *
      * @param format
+     *            {@link String}
      * @param args
+     *            format args
      * @see Formatter
      */
-    protected void log(final String format, final Object... args) {
+    protected void log(String format, Object... args) {
         log(verbosity, format, args);
     }
 
@@ -80,23 +76,30 @@ abstract class LoggingTask extends Task implements ProjectUtils {
      * Log a formatted message at the specified level.
      *
      * @param level
+     *            log level
      * @param format
+     *            {@link String}
      * @param args
+     *            format args
      * @see Formatter
      */
-    protected void log(final int level, final String format, final Object... args) {
+    protected void log(int level, String format, Object... args) {
         log(String.format(format, args), level);
     }
 
     /**
-     * Log a formatted message with accompanying/triggering {@link Throwable}.
+     * Log a formatted message at {@link Project#MSG_ERR} level with
+     * accompanying/triggering {@link Throwable}.
      *
      * @param t
+     *            {@link Throwable}
      * @param format
+     *            {@link String}
      * @param args
+     *            format args
      * @see Formatter
      */
-    protected void log(final Throwable t, final String format, final Object... args) {
+    protected void log(Throwable t, String format, Object... args) {
         log(Project.MSG_ERR, t, format, args);
     }
 
@@ -105,14 +108,16 @@ abstract class LoggingTask extends Task implements ProjectUtils {
      * a specific level.
      *
      * @param level
+     *            log level
      * @param t
+     *            {@link Throwable}
      * @param format
+     *            {@link String}
      * @param args
+     *            format args
      * @see Formatter
      */
-    protected void log(final int level, final Throwable t, final String format,
-        final Object... args) {
+    protected void log(int level, Throwable t, String format, Object... args) {
         log(String.format(format, args), t, level);
     }
-
 }
\ No newline at end of file
diff --git a/src/main/org/apache/ant/s3/ObjectResource.java b/src/main/org/apache/ant/s3/ObjectResource.java
index f0653fc..585243b 100644
--- a/src/main/org/apache/ant/s3/ObjectResource.java
+++ b/src/main/org/apache/ant/s3/ObjectResource.java
@@ -74,8 +74,7 @@ public class ObjectResource extends Resource implements ProjectUtils {
 
     @FunctionalInterface
     private interface Finalizer {
-        static final Finalizer NOP = () -> {
-        };
+        static final Finalizer NOP = () -> {};
 
         void run() throws Exception;
 
@@ -115,58 +114,122 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * Create a new {@link ObjectResource}.
      *
      * @param project
+     *            Ant {@link Project}
      */
     public ObjectResource(Project project) {
         setProject(project);
     }
 
     /**
-     * Create a new {@link ObjectResource} from a listing.
+     * Create an {@link ObjectResource} from a listing.
      *
      * @param project
+     *            Ant {@link Project}
      * @param s3
+     *            {@link S3Client}
      * @param bucket
+     *            of object
      * @param summary
-     * @param precision
+     *            object info
      */
     ObjectResource(Project project, S3Client s3, String bucket, S3Object summary) {
         this(project, s3, bucket, summary.key(), summary::size, summary::lastModified, null, Precision.object);
     }
 
+    /**
+     * Create an {@link ObjectResource} representing a delete marker.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @param s3
+     *            {@link S3Client}
+     * @param bucket
+     *            of object
+     * @param deleteMarker
+     *            info
+     */
     ObjectResource(Project project, S3Client s3, String bucket, DeleteMarkerEntry deleteMarker) {
         this(project, s3, bucket, deleteMarker.key(), () -> UNKNOWN_SIZE, deleteMarker::lastModified,
             deleteMarker::versionId, Precision.version);
         versionInfo = Optional.ofNullable(new VersionInfo(true, deleteMarker.isLatest().booleanValue()));
     }
 
+    /**
+     * Create an {@link ObjectResource} representing an object version.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @param s3
+     *            {@link S3Client}
+     * @param bucket
+     *            of object
+     * @param version
+     *            info
+     */
     ObjectResource(Project project, S3Client s3, String bucket, ObjectVersion version) {
         this(project, s3, bucket, version.key(), version::size, version::lastModified, version::versionId,
             Precision.version);
         versionInfo = Optional.of(new VersionInfo(false, version.isLatest().booleanValue()));
     }
 
+    /**
+     * Create an {@link ObjectResource}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @param s3
+     *            {@link S3Client}
+     * @param bucket
+     *            of object
+     * @param key
+     *            of object
+     * @param size
+     *            of object
+     * @param lastModified
+     *            of object
+     * @param versionId
+     *            of object
+     * @param precision
+     *            of object
+     */
     ObjectResource(Project project, S3Client s3, String bucket, String key, LongSupplier size,
         Supplier<Instant> lastModified, Supplier<String> versionId, Precision precision) {
-
         this(project, s3, bucket, key);
         this.size = size;
         this.lastModified = lastModified;
         this.versionId = versionId;
     }
 
+    /**
+     * Create an {@link ObjectResource} representing a prefix.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @param s3
+     *            {@link S3Client}
+     * @param bucket
+     *            of prefix
+     * @param prefix
+     *            {@link String}
+     * @return {@link ObjectResource}
+     */
     static ObjectResource ofPrefix(Project project, S3Client s3, String bucket, String prefix) {
-        ObjectResource result = new ObjectResource(project,s3,bucket,prefix);
+        final ObjectResource result = new ObjectResource(project, s3, bucket, prefix);
         result._setDirectory(true);
         return result;
     }
 
     /**
-     * Create a new {@link ObjectResource} fully-formed.
+     * Create a new {@link ObjectResource} with complete identifying info.
      *
      * @param project
+     *            Ant {@link Project}
      * @param s3
+     *            {@link S3Client}
      * @param bucket
+     *            of object
      * @param key
+     *            of object
      */
     ObjectResource(Project project, S3Client s3, String bucket, String key) {
         setProject(project);
@@ -188,6 +251,7 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * Set the bucket of the S3 object.
      *
      * @param bucket
+     *            of object
      */
     public void setBucket(String bucket) {
         checkAttributesAllowed();
@@ -210,6 +274,7 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * Set the key of the S3 object within its bucket.
      *
      * @param key
+     *            of object
      */
     public void setKey(String key) {
         this.key = key;
@@ -218,6 +283,9 @@ public class ObjectResource extends Resource implements ProjectUtils {
     /**
      * Set the name of the S3 object, which for our purposes is equivalent to
      * calling {@link #setKey(String)}.
+     * 
+     * @param name
+     *            {@code key}
      */
     @Override
     public void setName(String name) {
@@ -228,6 +296,8 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * Get the name of the S3 object, which may be suffixed with the object
      * version if {@link #getPrecision()} returns {@link Precision#version} and
      * this object exists.
+     * 
+     * @return {@link String}
      */
     @Override
     public String getName() {
@@ -244,7 +314,9 @@ public class ObjectResource extends Resource implements ProjectUtils {
 
     /**
      * {@inheritDoc}
+     * 
      * @throws UnsupportedOperationException
+     *             always
      */
     @Override
     public void setDirectory(boolean directory) {
@@ -295,6 +367,7 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * Set the content type of the S3 object.
      *
      * @param contentType
+     *            of object
      */
     public void setContentType(String contentType) {
         this.contentType = contentType;
@@ -339,6 +412,7 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * Set the metadata.
      *
      * @param metadata
+     *            of object
      */
     public void setMetadata(Map<String, String> metadata) {
         Exceptions.raiseIf(isReference(), UnsupportedOperationException::new, "setMetadata");
@@ -386,6 +460,7 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * Set the tagging for the S3 object.
      *
      * @param tagging
+     *            of object
      */
     public void setTagging(Map<String, String> tagging) {
         if (isReference()) {
@@ -398,6 +473,7 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * Add the configured {@link Client} element.
      *
      * @param s3
+     *            {@link Client}
      */
     public void addConfigured(Client s3) {
         checkChildrenAllowed();
@@ -412,11 +488,11 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * Set the {@link Client} by reference.
      *
      * @param refid
+     *            of {@link Client}
      */
     public void setClientRefid(String refid) {
         checkAttributesAllowed();
-        Exceptions.raiseIf(StringUtils.isBlank(refid), buildException(),
-            "@clientrefid must not be null/empty/blank");
+        Exceptions.raiseIf(StringUtils.isBlank(refid), buildException(), "@clientrefid must not be null/empty/blank");
 
         addConfigured(getProject().<Client> getReference(refid));
     }
@@ -521,6 +597,7 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * Put a {@link File} as the content of this S3 object.
      *
      * @param file
+     *            source
      */
     public void put(File file) {
         if (isReference()) {
@@ -535,7 +612,9 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * {@link S3Client} client to use.
      *
      * @param s3
+     *            {@link S3Client} to use
      * @param file
+     *            source
      */
     public void put(S3Client s3, File file) {
         if (isReference()) {
@@ -558,6 +637,7 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * Delete this object from its bucket using the specified {@link S3Client}.
      * 
      * @param s3
+     *            {@link S3Client} to use
      */
     public void delete(S3Client s3) {
         if (isReference()) {
@@ -597,6 +677,7 @@ public class ObjectResource extends Resource implements ProjectUtils {
      * data are equal, in addition to {@link Object#equals(Object)} equality.
      *
      * @param other
+     *            object to compare for equality
      * @return {@code boolean}
      */
     public boolean fullyEquals(Object other) {
@@ -709,4 +790,3 @@ public class ObjectResource extends Resource implements ProjectUtils {
         super.setDirectory(directory);
     }
 }
-
diff --git a/src/main/org/apache/ant/s3/ObjectResources.java b/src/main/org/apache/ant/s3/ObjectResources.java
index ca40fc2..a447952 100644
--- a/src/main/org/apache/ant/s3/ObjectResources.java
+++ b/src/main/org/apache/ant/s3/ObjectResources.java
@@ -73,6 +73,7 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
      * Create a new {@link ObjectResources} instance.
      *
      * @param project
+     *            Ant {@link Project}
      */
     public ObjectResources(final Project project) {
         super(project);
@@ -84,6 +85,7 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
      * Add the nested {@link Client}.
      *
      * @param s3
+     *            {@link Client}
      */
     public void addConfigured(final Client s3) {
         checkChildrenAllowed();
@@ -99,6 +101,7 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
      * Set {@link Client} by reference.
      *
      * @param refid
+     *            of {@link Client}
      */
     public void setClientRefid(final String refid) {
         checkAttributesAllowed();
@@ -113,6 +116,7 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
      * Add a configured {@link ResourceSelector}.
      *
      * @param selector
+     *            to add
      */
     public void addConfigured(ResourceSelector selector) {
         checkChildrenAllowed();
@@ -215,6 +219,7 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
      * Set the bucket of this {@link ObjectResources}.
      *
      * @param bucket
+     *            where resources exist
      */
     public void setBucket(String bucket) {
         checkAttributesAllowed();
@@ -240,13 +245,13 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
      * name property and {@link Object#toString()} value of generated
      * {@link ObjectResource}s.
      * 
-     * @param precision
+     * @param as
      *            {@link Precision}
      */
-    public void setAs(Precision precision) {
+    public void setAs(Precision as) {
         checkAttributesAllowed();
-        if (precision != this.as) {
-            this.as = Objects.requireNonNull(precision);
+        if (as != this.as) {
+            this.as = Objects.requireNonNull(as);
             resetResourceCache();
         }
     }
@@ -314,6 +319,7 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
      * default {@code true}.
      *
      * @param cache
+     *            flag
      */
     public void setCache(boolean cache) {
         this.cache = cache;
@@ -335,7 +341,9 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
     /**
      * Set whether to include S3 object prefixes as "directory" type
      * {@link Resource}s, default {@code false}.
-     * @param includePrefixes {@code boolean}
+     * 
+     * @param includePrefixes
+     *            {@code boolean}
      */
     public void setIncludePrefixes(boolean includePrefixes) {
         checkAttributesAllowed();
@@ -349,6 +357,7 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
      * Add a nested {@link PatternSet}.
      *
      * @param patternSet
+     *            to add
      */
     public void addConfigured(PatternSet patternSet) {
         checkChildrenAllowed();
@@ -404,6 +413,7 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
      * Set includes patterns via attribute.
      *
      * @param includes
+     *            patterns
      */
     public void setIncludes(String includes) {
         checkAttributesAllowed();
@@ -415,6 +425,7 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
      * Set includes file via attribute.
      *
      * @param includesFile
+     *            {@link File}
      */
     public void setIncludesFile(File includesFile) {
         checkAttributesAllowed();
@@ -423,9 +434,10 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
     }
 
     /**
-     * Set excludes pattersn via attribute.
+     * Set excludes patterns via attribute.
      *
      * @param excludes
+     *            patterns
      */
     public void setExcludes(String excludes) {
         checkAttributesAllowed();
@@ -437,6 +449,7 @@ public class ObjectResources extends S3DataType implements ResourceCollection {
      * Set excludes file via attribute.
      *
      * @param excludesFile
+     *            {@link File}
      */
     public void setExcludesFile(File excludesFile) {
         checkAttributesAllowed();
diff --git a/src/main/org/apache/ant/s3/ProjectUtils.java b/src/main/org/apache/ant/s3/ProjectUtils.java
index fa6cd4f..e4a739a 100644
--- a/src/main/org/apache/ant/s3/ProjectUtils.java
+++ b/src/main/org/apache/ant/s3/ProjectUtils.java
@@ -35,7 +35,7 @@ interface ProjectUtils {
     /**
      * Attempt to determine a component name for the specified type.
      * 
-     * @param type
+     * @param type for which component name is desired
      *
      * @return {@link String}
      */
@@ -47,8 +47,8 @@ interface ProjectUtils {
      * Attempt to determine a component name for the specified type relative to
      * the specified {@link Project}.
      *
-     * @param project
-     * @param type
+     * @param project Ant project
+     * @param type for which component name is desired
      * @return {@link String}
      */
     default String componentName(final Project project, final Class<?> type) {
@@ -68,9 +68,9 @@ interface ProjectUtils {
     /**
      * Require the specified item.
      *
-     * @param <T>
-     * @param item
-     * @param description
+     * @param <T> type
+     * @param item required object
+     * @param description description in thrown {@link Exception} if absent
      * @return {@code item}
      * @throws IllegalStateException
      *             if {@code t == null}
@@ -83,10 +83,10 @@ interface ProjectUtils {
     /**
      * Require the specified component.
      * 
-     * @param component
-     * @param type
+     * @param component required component
+     * @param type of component
      *
-     * @param <T>
+     * @param <T> type
      * @return {@code component}
      */
     default <T> T requireComponent(final T component, final Class<?> type) {
@@ -97,7 +97,6 @@ interface ProjectUtils {
      * Create a {@link Function} that will create a {@link BuildException} at
      * the specified {@link Location}, given the {@link String} message.
      *
-     * @param location
      * @return {@link Function}
      */
     default Function<String, BuildException> buildException() {
@@ -109,7 +108,6 @@ interface ProjectUtils {
      * the specified {@link Location}, given the {@link String} message and
      * {@link Throwable} cause.
      *
-     * @param location
      * @return {@link BiFunction}
      */
     default BiFunction<String, Throwable, BuildException> buildExceptionTriggered() {
diff --git a/src/main/org/apache/ant/s3/Put.java b/src/main/org/apache/ant/s3/Put.java
index 898e376..109e4c4 100644
--- a/src/main/org/apache/ant/s3/Put.java
+++ b/src/main/org/apache/ant/s3/Put.java
@@ -23,6 +23,7 @@ import java.util.Vector;
 import java.util.function.Supplier;
 
 import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
 import org.apache.tools.ant.types.FilterChain;
 import org.apache.tools.ant.types.FilterSetCollection;
 import org.apache.tools.ant.types.Resource;
@@ -41,9 +42,20 @@ public class Put extends CopyResources {
     private String bucket;
 
     /**
+     * Create a new {@link Put} instance.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     */
+    public Put(Project project) {
+        super(project);
+    }
+
+    /**
      * Add a configured {@link Client}.
      *
      * @param s3
+     *            {@link Client}
      */
     public void addConfigured(Client s3) {
         if (this.s3 != null) {
@@ -56,6 +68,7 @@ public class Put extends CopyResources {
      * Set the {@link Client} by reference.
      *
      * @param refid
+     *            of {@link Client}
      */
     public void setClientRefid(String refid) {
         Objects.requireNonNull(StringUtils.trimToNull(refid), "@clientrefid must not be null/empty/blank");
@@ -76,6 +89,7 @@ public class Put extends CopyResources {
      * Set the bucket to which target objects should be written.
      *
      * @param bucket
+     *            target
      */
     public void setBucket(String bucket) {
         this.bucket = StringUtils.trimToNull(bucket);
@@ -85,6 +99,7 @@ public class Put extends CopyResources {
      * Disable {@code append}.
      *
      * @param append
+     *            flag
      * @throws BuildException
      *             if {@code append == true}
      */
@@ -97,6 +112,7 @@ public class Put extends CopyResources {
      * Disable {@code preserveLastModified}.
      *
      * @param preserveLastModified
+     *            flag
      * @throws BuildException
      *             if {@code preserveLastModified == true}
      */
@@ -117,9 +133,10 @@ public class Put extends CopyResources {
     }
 
     /**
-     * Disable {@code overwrite}.
+     * Force {@code overwrite}.
      * 
      * @param overwrite
+     *            flag
      * @throws BuildException
      *             if {@code overwrite == false}
      */
diff --git a/src/main/org/apache/ant/s3/S3DataType.java b/src/main/org/apache/ant/s3/S3DataType.java
index f2c38f5..fe805e2 100644
--- a/src/main/org/apache/ant/s3/S3DataType.java
+++ b/src/main/org/apache/ant/s3/S3DataType.java
@@ -28,18 +28,17 @@ public abstract class S3DataType extends DataType implements ProjectUtils {
     /**
      * Create a new {@link S3DataType} instance.
      *
-     * @param project
+     * @param project Ant {@link Project}
      */
     protected S3DataType(final Project project) {
         setProject(project);
     }
 
     /**
-     * Log a formatted message at the default level dictated by
-     * {@link #isVerbose()}.
+     * Log a formatted message at {@link Project#MSG_INFO} level.
      *
-     * @param format
-     * @param args
+     * @param format {@link String}
+     * @param args to format
      * @see Formatter
      */
     protected void log(final String format, final Object... args) {
@@ -49,9 +48,9 @@ public abstract class S3DataType extends DataType implements ProjectUtils {
     /**
      * Log a formatted message at the specified level.
      *
-     * @param level
-     * @param format
-     * @param args
+     * @param level log level
+     * @param format {@link String}
+     * @param args to format
      * @see Formatter
      */
     protected void log(final int level, final String format, final Object... args) {

[ant-antlibs-s3] 02/07: upgrade AWS SDK

Posted by mb...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

mbenson pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-s3.git

commit 2758938aff1976cd97ab57b34cff0866da7079b4
Author: Matt Benson <mb...@apache.org>
AuthorDate: Mon Mar 28 14:07:59 2022 -0500

    upgrade AWS SDK
---
 build.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.properties b/build.properties
index 164c8f2..5a6dd23 100644
--- a/build.properties
+++ b/build.properties
@@ -1,2 +1,2 @@
 ivy.install.version=2.5.0
-aws.sdk.version=2.17.129
+aws.sdk.version=2.17.155

[ant-antlibs-s3] 07/07: total overhaul of SDK building

Posted by mb...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

mbenson pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-s3.git

commit a6252af4eb247f73f229f4fbc61af71ea7538160
Author: Matt Benson <mb...@apache.org>
AuthorDate: Thu Mar 31 09:20:20 2022 -0500

    total overhaul of SDK building
---
 ivy.xml                                            |  10 +-
 src/main/org/apache/ant/s3/Builder.java            | 208 ---------
 src/main/org/apache/ant/s3/Client.java             |  79 +---
 src/main/org/apache/ant/s3/Credentials.java        | 122 -----
 src/main/org/apache/ant/s3/Exceptions.java         |   2 +-
 src/main/org/apache/ant/s3/HttpConfiguration.java  |  57 ---
 src/main/org/apache/ant/s3/InlineProperties.java   |   9 +
 src/main/org/apache/ant/s3/ProjectUtils.java       |   2 +-
 src/main/org/apache/ant/s3/StringConversions.java  | 176 -------
 .../ant/s3/build/AwsStringConversionsProvider.java |  43 ++
 .../org/apache/ant/s3/build/BuildableSupplier.java | 133 ++++++
 src/main/org/apache/ant/s3/build/Buildables.java   |  94 ++++
 src/main/org/apache/ant/s3/build/Builder.java      | 266 +++++++++++
 src/main/org/apache/ant/s3/build/ClassFinder.java  | 138 ++++++
 .../apache/ant/s3/build/ConfigurableSupplier.java  |  77 ++++
 .../ant/s3/build/ConfigurableSupplierFactory.java  |  76 +++
 .../apache/ant/s3/build/ConfiguringSupplier.java   |  39 ++
 .../apache/ant/s3/build/ConfiguringSuppliers.java  |  96 ++++
 .../s3/build/DefaultStringConversionsProvider.java | 116 +++++
 .../org/apache/ant/s3/build/MetaBuilderByType.java | 140 ++++++
 .../org/apache/ant/s3/build/MethodSignature.java   | 126 +++++
 .../ant/s3/build/RootConfiguringSupplier.java      | 116 +++++
 .../org/apache/ant/s3/build/StringConversions.java | 248 ++++++++++
 .../s3/build/spi/ConfiguringSuppliersProvider.java |  76 +++
 .../apache/ant/s3/build/spi/DefaultProvider.java   |  33 ++
 .../s3/build/spi/IntrospectingProviderBase.java    | 183 ++++++++
 .../org/apache/ant/s3/build/spi/Providers.java     |  64 +++
 .../s3/build/spi/StringConversionsProvider.java    |  94 ++++
 .../CredentialsConfiguringSuppliersProvider.java   | 513 +++++++++++++++++++++
 .../http/ClientConfiguringSuppliersProvider.java   |  82 ++++
 src/main/org/apache/ant/s3/strings/ClassNames.java | 234 ++++++++++
 .../org/apache/ant/s3/strings/PackageNames.java    | 214 +++++++++
 src/main/org/apache/ant/s3/strings/Strings.java    | 128 +++++
 src/tests/antunit/s3-test-base.xml                 |   9 +-
 .../apache/ant/s3/build/StringConversionsTest.java | 156 +++++++
 .../org/apache/ant/s3/strings/ClassNamesTest.java  | 345 ++++++++++++++
 .../apache/ant/s3/strings/PackageNamesTest.java    | 286 ++++++++++++
 .../org/apache/ant/s3/strings/StringsTest.java     | 114 +++++
 38 files changed, 4256 insertions(+), 648 deletions(-)

diff --git a/ivy.xml b/ivy.xml
index ceaaed1..e84e2ac 100644
--- a/ivy.xml
+++ b/ivy.xml
@@ -32,7 +32,8 @@
   </info>
   <configurations defaultconfmapping="*->default">
     <conf name="default" description="full antlib with all dependencies" />
-    <conf name="provided" description="Ant must be present at runtime" />
+    <conf name="provided" description="Ant must be present at runtime" visibility="private" />
+    <conf name="sso" description="Optional SSO support" />
     <conf name="test" description="dependencies used for tests of the antlib" visibility="private" />
   </configurations>
   <publications xmlns:e="urn:ant.apache.org:ivy-extras">
@@ -55,15 +56,20 @@
   </publications>
   <dependencies defaultconfmapping="*->default">
     <dependency org="software.amazon.awssdk" name="s3" rev="${aws.sdk.version}" conf="default">
-      <exclude org="software.amazon.awssdk" name="apache-client" />
     </dependency>
     <dependency org="software.amazon.awssdk" name="url-connection-client" rev="${aws.sdk.version}" conf="default" />
+    <dependency org="software.amazon.awssdk" name="sts" rev="${aws.sdk.version}" conf="default" />
     <dependency org="org.apache.commons" name="commons-lang3" rev="3.12.0" conf="default" />
+    <dependency org="org.kohsuke.metainf-services" name="metainf-services" rev="1.8" conf="default" />
     <dependency org="org.apache.ant" name="ant" rev="1.10.12" conf="provided" />
     <dependency org="junit" name="junit" rev="4.13" conf="test" />
     <dependency org="com.adobe.testing" name="s3mock" rev="2.4.7" conf="test" />
     <dependency org="jakarta.servlet.jsp" name="jakarta.servlet.jsp-api" rev="2.3.6" conf="test" />
     <dependency org="org.apache.groovy" name="groovy-ant" rev="4.0.0" transitive="false" conf="test" />
     <dependency org="org.apache.groovy" name="groovy-jsr223" rev="4.0.0" conf="test" />
+    <dependency org="org.assertj" name="assertj-core" rev="3.22.0" conf="test" />
+    <dependency org="software.amazon.awssdk" name="aws-json-protocol" rev="${aws.sdk.version}" conf="sso" transitive="false" />
+    <dependency org="software.amazon.awssdk" name="sso" rev="${aws.sdk.version}" conf="sso" transitive="false" />
+    <exclude org="software.amazon.awssdk" artifact="apache-client" />
   </dependencies>
 </ivy-module>
diff --git a/src/main/org/apache/ant/s3/Builder.java b/src/main/org/apache/ant/s3/Builder.java
deleted file mode 100644
index db68725..0000000
--- a/src/main/org/apache/ant/s3/Builder.java
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.ant.s3;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
-import java.lang.reflect.TypeVariable;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.BiPredicate;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Supplier;
-
-import org.apache.commons.lang3.ClassUtils;
-import org.apache.commons.lang3.ClassUtils.Interfaces;
-import org.apache.tools.ant.BuildException;
-import org.apache.tools.ant.DynamicAttributeNS;
-import org.apache.tools.ant.DynamicElementNS;
-import org.apache.tools.ant.Project;
-
-import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder;
-import software.amazon.awssdk.http.SdkHttpClient;
-
-/**
- * Support AWS SDK v2 fluent builder conventions.
- *
- * @param <T>
- */
-public class Builder<T> extends S3DataType implements Consumer<T>, DynamicAttributeNS, DynamicElementNS {
-    private static class Parameter {
-        final Method mutator;
-        final Object value;
-
-        Parameter(Method mutator, Object value) {
-            this.mutator = mutator;
-            this.value = value;
-        }
-    }
-
-    private static final Map<Class<?>, Supplier<?>> SUPPLIERS;
-    private static final Set<BiPredicate<String, String>> NAME_COMPARISONS =
-        Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(String::equals, String::equalsIgnoreCase)));
-
-    static {
-        final Map<Class<?>, Supplier<?>> suppliers = new LinkedHashMap<>();
-        suppliers.put(SdkHttpClient.Builder.class, DefaultSdkHttpClientBuilder::new);
-        SUPPLIERS = Collections.unmodifiableMap(suppliers);
-    }
-
-    private static boolean isEquivalentTo(Class<?> c, Type t) {
-        if (ParameterizedType.class.isInstance(t)) {
-            t = ((ParameterizedType) t).getRawType();
-        }
-        return c.equals(t);
-    }
-
-    private static boolean isFluent(Method m) {
-        final Class<?> declaringClass = m.getDeclaringClass();
-
-        final Type genericReturnType = m.getGenericReturnType();
-
-        if (isEquivalentTo(declaringClass, genericReturnType)) {
-            return true;
-        }
-        if (TypeVariable.class.isInstance(genericReturnType)) {
-            final TypeVariable<?> var = (TypeVariable<?>) genericReturnType;
-            if (var.getGenericDeclaration().equals(declaringClass)) {
-                ;
-            }
-            for (final Type bound : var.getBounds()) {
-                if (isEquivalentTo(declaringClass, bound)) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    private static boolean isFluentSdkMutator(Method m) {
-        return isFluent(m) && !Modifier.isStatic(m.getModifiers()) && m.getParameterTypes().length == 1;
-    }
-
-    private static Parameter parameter(Class<?> c, String name, String value) {
-        return searchMethods(c, name, m -> {
-            try {
-                return Optional.of(new Parameter(m, StringConversions.as(m.getParameterTypes()[0], value)));
-            } catch (IllegalArgumentException e) {
-                return Optional.empty();
-            }
-        });
-    }
-
-    private static Method element(Class<?> c, String name) {
-        return searchMethods(c, name, m -> {
-            return Optional.of(m).filter(o -> {
-                final Class<?> pt = m.getParameterTypes()[0];
-                return Consumer.class.equals(pt) || SUPPLIERS.containsKey(pt);
-            });
-        });
-    }
-
-    private static <R> R searchMethods(Class<?> c, String name, Function<Method, Optional<R>> fn) {
-        for (BiPredicate<String, String> nameComparison : NAME_COMPARISONS) {
-
-            for (Class<?> type : ClassUtils.hierarchy(c, Interfaces.INCLUDE)) {
-                for (Method m : type.getDeclaredMethods()) {
-                    if (nameComparison.test(name, m.getName()) && isFluentSdkMutator(m)) {
-                        final Optional<R> result = fn.apply(m);
-                        if (result.isPresent()) {
-                            return result.get();
-                        }
-                    }
-                }
-            }
-        }
-        throw new IllegalArgumentException(name);
-    }
-
-    private final Class<T> target;
-    private final Set<Parameter> parameters = new LinkedHashSet<>();
-    private final Map<Method, Builder<?>> elements = new LinkedHashMap<>();
-
-    /**
-     * Create a new {@link Builder} instance.
-     *
-     * @param target
-     *            type
-     */
-    public Builder(Class<T> target, Project project) {
-        super(project);
-        this.target = Objects.requireNonNull(target, "target");
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void accept(T t) {
-        parameters.forEach(p -> {
-            try {
-                p.mutator.invoke(t, p.value);
-            } catch (IllegalAccessException | InvocationTargetException e) {
-                throw new IllegalStateException(e);
-            }
-        });
-        elements.forEach((m, b) -> {
-            final Class<?> pt = m.getParameterTypes()[0];
-            final Object arg;
-            if (Consumer.class.equals(pt)) {
-                arg = b;
-            } else {
-                arg = SUPPLIERS.get(pt).get();
-
-                @SuppressWarnings("unchecked")
-                final Consumer<Object> cmer = (Consumer<Object>) b;
-                cmer.accept(arg);
-            }
-            try {
-                m.invoke(t, arg);
-            } catch (IllegalAccessException | InvocationTargetException e) {
-                throw new IllegalStateException(e);
-            }
-        });
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setDynamicAttribute(String uri, String localName, String qName, String value) throws BuildException {
-        parameters.add(parameter(target, localName, value));
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Object createDynamicElement(String uri, String localName, String qName) throws BuildException {
-        final Method m = element(target, localName);
-        final Builder<?> result = new Builder<>(m.getParameterTypes()[0], getProject());
-        elements.put(m, result);
-        return result;
-    }
-}
diff --git a/src/main/org/apache/ant/s3/Client.java b/src/main/org/apache/ant/s3/Client.java
index 7c550aa..42e3c77 100644
--- a/src/main/org/apache/ant/s3/Client.java
+++ b/src/main/org/apache/ant/s3/Client.java
@@ -16,96 +16,23 @@
  */
 package org.apache.ant.s3;
 
-import java.util.Objects;
-import java.util.Optional;
-import java.util.function.Supplier;
-import java.util.stream.Stream;
-
-import org.apache.tools.ant.BuildException;
+import org.apache.ant.s3.build.RootConfiguringSupplier;
 import org.apache.tools.ant.Project;
 import org.apache.tools.ant.types.DataType;
 
 import software.amazon.awssdk.services.s3.S3Client;
-import software.amazon.awssdk.services.s3.S3ClientBuilder;
 
 /**
  * {@link DataType} providing access to an {@link S3Client} instance.
  */
-public class Client extends S3DataType implements Supplier<S3Client> {
-
-    private Builder<S3ClientBuilder> builder;
-    private Credentials credentials;
-    private HttpConfiguration httpConfiguration;
+public class Client extends RootConfiguringSupplier<S3Client> {
 
     /**
      * Create a new {@link Client}.
      *
-     * @param project
+     * @param project Ant {@link Project}
      */
     public Client(Project project) {
         super(project);
     }
-
-    /**
-     * Create a nested {@code builder} element to allow customization.
-     *
-     * @return {@link AmazonS3ClientBuilder}
-     */
-    public Builder<S3ClientBuilder> createBuilder() {
-        checkChildrenAllowed();
-
-        if (builder != null) {
-            singleElementAllowed("builder");
-        }
-        return builder = new Builder<>(S3ClientBuilder.class, getProject());
-    }
-
-    /**
-     * Create a nested {@link Credentials} element.
-     *
-     * @return {@link Credentials}
-     */
-    public Credentials createCredentials() {
-        checkChildrenAllowed();
-
-        if (credentials != null) {
-            singleElementAllowed("credentials");
-        }
-        return credentials = new Credentials(getProject());
-    }
-
-    /**
-     * Create a nested {@link HttpConfiguration} element.
-     *
-     * @return {@link HttpConfiguration}
-     */
-    public HttpConfiguration createHttp() {
-        checkChildrenAllowed();
-
-        if (httpConfiguration != null) {
-            singleElementAllowed("http");
-        }
-        return httpConfiguration = new HttpConfiguration(getProject());
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public S3Client get() {
-        if (isReference()) {
-            return getRefid().<Client> getReferencedObject().get();
-        }
-        final S3ClientBuilder scb = S3Client.builder();
-        Optional.ofNullable(builder).ifPresent(bb -> bb.accept(scb));
-
-        Stream.of(credentials, httpConfiguration).filter(Objects::nonNull).forEach(c -> c.accept(scb));
-
-        return scb.build();
-    }
-
-    private void singleElementAllowed(final String name) {
-        throw new BuildException(String.format("%s permits a single nested %s element", getDataTypeName(), name),
-            getLocation());
-    }
 }
diff --git a/src/main/org/apache/ant/s3/Credentials.java b/src/main/org/apache/ant/s3/Credentials.java
deleted file mode 100644
index 3efd63b..0000000
--- a/src/main/org/apache/ant/s3/Credentials.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.ant.s3;
-
-import java.util.function.Consumer;
-
-import org.apache.tools.ant.Project;
-import org.apache.tools.ant.util.StringUtils;
-
-import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
-import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
-import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
-import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
-import software.amazon.awssdk.services.s3.S3ClientBuilder;
-
-/**
- * AWS credentials configuration. {@pcode profile} is preferred to
- * {@code accessKey}/{@code secretKey}.
- */
-public class Credentials extends S3DataType implements Consumer<S3ClientBuilder> {
-    private String accessKey;
-    private String secretKey;
-    private String profile;
-
-    /**
-     * Create a new {@link Credentials} instance.
-     *
-     * @param project
-     */
-    public Credentials(final Project project) {
-        super(project);
-    }
-
-    /**
-     * Get the access key.
-     *
-     * @return {@link String}
-     */
-    public String getAccessKey() {
-        return accessKey;
-    }
-
-    /**
-     * Set the access key.
-     *
-     * @param accessKey
-     */
-    public void setAccessKey(final String accessKey) {
-        this.accessKey = StringUtils.trimToNull(accessKey);
-    }
-
-    /**
-     * Get the secret key.
-     *
-     * @return {@link String}
-     */
-    public String getSecretKey() {
-        return secretKey;
-    }
-
-    /**
-     * Set the secret key.
-     *
-     * @param secretKey
-     */
-    public void setSecretKey(final String secretKey) {
-        this.secretKey = StringUtils.trimToNull(secretKey);
-    }
-
-    /**
-     * Get the desired profile.
-     *
-     * @return {@link String}
-     */
-    public String getProfile() {
-        return profile;
-    }
-
-    /**
-     * Set the desired profile.
-     *
-     * @param profile
-     */
-    public void setProfile(final String profile) {
-        this.profile = StringUtils.trimToNull(profile);
-    }
-
-    /**
-     * Apply settings to {@code builder}.
-     *
-     * @param builder
-     */
-    @Override
-    public void accept(S3ClientBuilder builder) {
-        final AwsCredentialsProvider credentialsProvider;
-
-        if (getProfile() == null) {
-            Exceptions.raiseIf(getAccessKey() == null || getSecretKey() == null, buildException(),
-                "%s requires both @accessKey and @secretKey in the absence of @profile", getDataTypeName());
-
-            credentialsProvider =
-                StaticCredentialsProvider.create(AwsBasicCredentials.create(getAccessKey(), getSecretKey()));
-        } else {
-            credentialsProvider = ProfileCredentialsProvider.create(getProfile());
-        }
-        builder.credentialsProvider(credentialsProvider);
-    }
-}
\ No newline at end of file
diff --git a/src/main/org/apache/ant/s3/Exceptions.java b/src/main/org/apache/ant/s3/Exceptions.java
index 5a0a164..bfe8ba9 100644
--- a/src/main/org/apache/ant/s3/Exceptions.java
+++ b/src/main/org/apache/ant/s3/Exceptions.java
@@ -24,7 +24,7 @@ import java.util.stream.Stream;
 /**
  * Utility class for the creation and throwing of {@link Exception}s.
  */
-class Exceptions {
+public class Exceptions {
 
     public static <E extends Exception> E create(final Function<? super String, ? extends E> fn, final String format,
         final Object... args) {
diff --git a/src/main/org/apache/ant/s3/HttpConfiguration.java b/src/main/org/apache/ant/s3/HttpConfiguration.java
deleted file mode 100644
index 4c6144c..0000000
--- a/src/main/org/apache/ant/s3/HttpConfiguration.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.ant.s3;
-
-import java.util.Map;
-import java.util.function.Consumer;
-
-import org.apache.tools.ant.Project;
-
-import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder;
-import software.amazon.awssdk.http.SdkHttpConfigurationOption;
-import software.amazon.awssdk.services.s3.S3ClientBuilder;
-import software.amazon.awssdk.utils.AttributeMap;
-
-/**
- * S3 client Http configuration.
- */
-public class HttpConfiguration extends InlineProperties implements Consumer<S3ClientBuilder> {
-
-    /**
-     * Create a new {@link HttpConfiguration} element.
-     *
-     * @param project
-     */
-    public HttpConfiguration(Project project) {
-        super(project);
-    }
-
-    /**
-     * Apply this {@link HttpConfiguration} to the specified
-     * {@link S3ClientBuilder}.
-     *
-     * @param b
-     */
-    @Override
-    public void accept(S3ClientBuilder b) {
-        @SuppressWarnings({ "unchecked", "rawtypes" })
-        final AttributeMap attributeMap =
-            StringConversions.attributes(SdkHttpConfigurationOption.class, (Map) properties);
-
-        b.httpClient(new DefaultSdkHttpClientBuilder().buildWithDefaults(attributeMap));
-    }
-}
diff --git a/src/main/org/apache/ant/s3/InlineProperties.java b/src/main/org/apache/ant/s3/InlineProperties.java
index 22f4f89..fc199f4 100644
--- a/src/main/org/apache/ant/s3/InlineProperties.java
+++ b/src/main/org/apache/ant/s3/InlineProperties.java
@@ -94,4 +94,13 @@ public class InlineProperties extends S3DataType implements DynamicElementNS {
     public InlineProperty createDynamicElement(String uri, String localName, String qName) {
         return new InlineProperty(localName);
     }
+
+    /**
+     * Get the managed {@link Properties} instance.
+     * 
+     * @return {@link Properties}
+     */
+    public Properties getProperties() {
+        return properties;
+    }
 }
\ No newline at end of file
diff --git a/src/main/org/apache/ant/s3/ProjectUtils.java b/src/main/org/apache/ant/s3/ProjectUtils.java
index e4a739a..d8e3bf2 100644
--- a/src/main/org/apache/ant/s3/ProjectUtils.java
+++ b/src/main/org/apache/ant/s3/ProjectUtils.java
@@ -30,7 +30,7 @@ import org.apache.tools.ant.Project;
 /**
  * Interface providing behavior for Ant {@link Project} components.
  */
-interface ProjectUtils {
+public interface ProjectUtils {
 
     /**
      * Attempt to determine a component name for the specified type.
diff --git a/src/main/org/apache/ant/s3/StringConversions.java b/src/main/org/apache/ant/s3/StringConversions.java
deleted file mode 100644
index 7705e85..0000000
--- a/src/main/org/apache/ant/s3/StringConversions.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.ant.s3;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.Type;
-import java.lang.reflect.TypeVariable;
-import java.time.Duration;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import org.apache.commons.lang3.reflect.TypeUtils;
-import org.apache.tools.ant.BuildException;
-import org.apache.tools.ant.Project;
-
-import software.amazon.awssdk.regions.Region;
-import software.amazon.awssdk.utils.AttributeMap;
-
-/**
- * Static utility class for conversions from {@link String} to needed types.
- */
-class StringConversions {
-    private static final TypeVariable<?> ATTRIBUTE_KEY_TYPE = AttributeMap.Key.class.getTypeParameters()[0];
-
-    private static final Map<Class<?>, Function<String, ?>> CONVERTERS;
-
-    private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER =
-        Stream
-            .of(Byte.class, Short.class, Integer.class, Character.class, Long.class, Float.class, Double.class,
-                Boolean.class)
-            .collect(Collectors.collectingAndThen(Collectors.<Class<?>, Class<?>, Class<?>> toMap(t -> {
-                try {
-                    return (Class<?>) t.getDeclaredField("TYPE").get(null);
-                } catch (IllegalAccessException | NoSuchFieldException e) {
-                    throw new IllegalStateException(e);
-                }
-            }, Function.identity()), Collections::unmodifiableMap));
-
-    static {
-        final Map<Class<?>, Function<String, ?>> cnv = new LinkedHashMap<>();
-        cnv.put(Byte.class, Byte::valueOf);
-        cnv.put(Short.class, Short::valueOf);
-        cnv.put(Integer.class, Integer::valueOf);
-        cnv.put(Character.class, s -> s.charAt(0));
-        cnv.put(Long.class, Long::valueOf);
-        cnv.put(Float.class, Float::valueOf);
-        cnv.put(Double.class, Double::valueOf);
-        cnv.put(Boolean.class, Boolean::valueOf);
-        cnv.put(String.class, Function.identity());
-        cnv.put(Region.class, Region::of);
-        cnv.put(Duration.class, Duration::parse);
-        CONVERTERS = Collections.unmodifiableMap(cnv);
-    }
-
-    private static Field keyField(Class<? extends AttributeMap.Key<?>> keyType, String name) {
-        try {
-            final Field result = keyType.getDeclaredField(name.toUpperCase(Locale.US));
-            Exceptions.raiseUnless(Modifier.isStatic(result.getModifiers()) && result.getType().equals(keyType),
-                IllegalArgumentException::new,
-                () -> String.format("Illegal %s key: %s", keyType.getSimpleName(), name));
-            return result;
-        } catch (NoSuchFieldException | SecurityException e) {
-            throw new BuildException(e);
-        }
-    }
-
-    /**
-     * Convert {@code value} to {@code type}.
-     *
-     * @param type
-     * @param value
-     * @return T
-     */
-    static <T> T as(Class<?> type, String value) {
-        if (type.isPrimitive()) {
-            type = PRIMITIVE_TO_WRAPPER.get(type);
-        }
-        final Optional<Object> converted = Optional.of(type).map(CONVERTERS::get).map(fn -> fn.apply(value));
-
-        if (converted.isPresent()) {
-            @SuppressWarnings("unchecked")
-            final T result = (T) converted.get();
-            return result;
-        }
-        if (type.isEnum()) {
-            try {
-                @SuppressWarnings({ "unchecked", "rawtypes" })
-                final T result = (T) Enum.valueOf((Class) type, value);
-                return result;
-            } catch (IllegalArgumentException e) {
-            }
-            @SuppressWarnings({ "unchecked", "rawtypes" })
-            final T result = (T) Enum.valueOf((Class) type, value.toUpperCase(Locale.US));
-            return result;
-        }
-        // Ant conventions
-        Constructor<T> ctor;
-        try {
-            @SuppressWarnings("unchecked")
-            final Constructor<T> _ctor = (Constructor<T>) type.getDeclaredConstructor(Project.class, String.class);
-            ctor = _ctor;
-        } catch (NoSuchMethodException | SecurityException e) {
-            try {
-                @SuppressWarnings("unchecked")
-                final Constructor<T> _ctor = (Constructor<T>) type.getDeclaredConstructor(String.class);
-                ctor = _ctor;
-            } catch (NoSuchMethodException | SecurityException e2) {
-                ctor = null;
-            }
-        }
-        if (ctor != null) {
-            try {
-                return ctor.newInstance(value);
-            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
-                throw new IllegalStateException(e);
-            }
-        }
-        throw new IllegalArgumentException();
-    }
-
-    /**
-     * Generate an AWS SDK {@link AttributeMap} from the specified
-     * {@link String} {@link Map} for the specified key type.
-     *
-     * @param <K>
-     * @param keyType
-     * @param m
-     * @return {@link AttributeMap}
-     */
-    static <K extends AttributeMap.Key<?>> AttributeMap attributes(Class<K> keyType, Map<String, String> m) {
-        final AttributeMap.Builder b = AttributeMap.builder();
-
-        m.forEach((k, v) -> {
-            final Field keyField = keyField(keyType, k);
-            final AttributeMap.Key<Object> key;
-            try {
-                @SuppressWarnings("unchecked")
-                final AttributeMap.Key<Object> _key = (AttributeMap.Key<Object>) keyField.get(null);
-                key = _key;
-            } catch (IllegalArgumentException | IllegalAccessException e) {
-                throw new BuildException(e);
-            }
-            final Type valueType =
-                TypeUtils.getTypeArguments(keyField.getGenericType(), AttributeMap.Key.class).get(ATTRIBUTE_KEY_TYPE);
-
-            final Object value = as(TypeUtils.getRawType(valueType, null), v);
-
-            b.<Object> put(key, value);
-        });
-
-        return b.build();
-    }
-}
diff --git a/src/main/org/apache/ant/s3/build/AwsStringConversionsProvider.java b/src/main/org/apache/ant/s3/build/AwsStringConversionsProvider.java
new file mode 100644
index 0000000..ba560e2
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/AwsStringConversionsProvider.java
@@ -0,0 +1,43 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import java.util.function.Function;
+
+import org.apache.ant.s3.build.spi.DefaultProvider;
+import org.apache.ant.s3.build.spi.StringConversionsProvider;
+import org.kohsuke.MetaInfServices;
+
+import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode;
+import software.amazon.awssdk.regions.Region;
+
+/**
+ * AWS {@link StringConversionsProvider}.
+ */
+@DefaultProvider
+@MetaInfServices
+public class AwsStringConversionsProvider extends StringConversionsProvider {
+    /**
+     * {@link Region} from {@link String}.
+     */
+    public static final Function<String, Region> regionOf = Region::of;
+
+    /**
+     * {@link DefaultsMode} from {@link String}.
+     */
+    public static final Function<String, DefaultsMode> defaultsModeFromValue = DefaultsMode::fromValue;
+}
diff --git a/src/main/org/apache/ant/s3/build/BuildableSupplier.java b/src/main/org/apache/ant/s3/build/BuildableSupplier.java
new file mode 100644
index 0000000..dc3c612
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/BuildableSupplier.java
@@ -0,0 +1,133 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import static software.amazon.awssdk.utils.FunctionalUtils.safeSupplier;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.function.Supplier;
+
+import org.apache.ant.s3.Exceptions;
+import org.apache.commons.lang3.reflect.MethodUtils;
+import org.apache.commons.lang3.reflect.TypeUtils;
+
+import software.amazon.awssdk.utils.builder.Buildable;
+
+/**
+ * Buildable supplier.
+ */
+public class BuildableSupplier<B extends Buildable, T> implements Supplier<B> {
+
+    @SuppressWarnings("rawtypes")
+    static final TypeVariable<Class<BuildableSupplier>> BUILDER = BuildableSupplier.class.getTypeParameters()[0];
+
+    @SuppressWarnings("rawtypes")
+    static final TypeVariable<Class<BuildableSupplier>> BUILT = BuildableSupplier.class.getTypeParameters()[1];
+
+    private static Type resolveAgainst(Class<?> t, TypeVariable<?> v) {
+        return TypeUtils.getTypeArguments(t, (Class<?>) v.getGenericDeclaration()).get(v);
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    static BuildableSupplier of(Method m) {
+        Exceptions.raiseUnless(Modifier.isStatic(m.getModifiers()), IllegalArgumentException::new,
+            "%s is not a static method", m);
+
+        Class<?> buildableType = m.getReturnType();
+
+        Supplier<Object> impl = safeSupplier(() -> m.invoke(null));
+
+        if (!Buildable.class.isAssignableFrom(buildableType)) {
+            Exceptions.raiseUnless(buildableType.isInterface(), IllegalArgumentException::new,
+                "%s returns type that is neither %s nor an interface", m, Buildable.class.getSimpleName());
+
+            Exceptions.raiseUnless(Buildables.quacksLikeABuildable(buildableType), IllegalArgumentException::new,
+                "%s returns type that is not duck-type %s", m, Buildable.class.getSimpleName());
+
+            final Method buildMethod = MethodUtils.getAccessibleMethod(buildableType, Buildables.BUILD_NAME);
+
+            final Class[] interfaces = new Class[] { buildableType, Buildable.class };
+            final ClassLoader ccl = Thread.currentThread().getContextClassLoader();
+
+            final Supplier wrapped = impl;
+            impl = () -> {
+                final Object target = wrapped.get();
+                return Proxy.newProxyInstance(ccl, interfaces, (proxy, method, args) -> {
+                    if (Buildables.BUILD_METHOD.test(method)) {
+                        method = buildMethod;
+                    }
+                    return method.invoke(target, args);
+                });
+            };
+            buildableType = Proxy.getProxyClass(ccl, interfaces);
+        }
+        final Class<?> builtType =
+            MethodUtils.getMatchingAccessibleMethod(buildableType, Buildables.BUILD_NAME).getReturnType();
+
+        return new BuildableSupplier(impl, buildableType, builtType);
+    }
+
+    final Supplier<B> impl;
+    final Class<B> builderType;
+    final Class<T> returnType;
+
+    @SuppressWarnings("unchecked")
+    protected BuildableSupplier(Supplier<B> impl) {
+        this.impl = impl;
+
+        this.builderType = (Class<B>) TypeUtils.getRawType(resolveAgainst(getClass(), Buildables.BUILDER), null)
+            .asSubclass(Buildable.class);
+
+        this.returnType = (Class<T>) TypeUtils.getRawType(resolveAgainst(getClass(), Buildables.BUILT), null);
+    }
+
+    BuildableSupplier(Supplier<B> impl, Class<B> builderType, Class<T> returnType) {
+        this.impl = impl;
+        this.builderType = builderType;
+        this.returnType = returnType;
+    }
+
+    /**
+     * Learn the {@link Buildable} type itself.
+     * 
+     * @return {@link Class} of {@code B}
+     */
+    public Class<B> getBuilderType() {
+        return builderType;
+    }
+
+    /**
+     * Learn the return type.
+     * 
+     * @return {@link Class}
+     */
+    public Class<T> getReturnType() {
+        return returnType;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public B get() {
+        return impl.get();
+    }
+}
\ No newline at end of file
diff --git a/src/main/org/apache/ant/s3/build/Buildables.java b/src/main/org/apache/ant/s3/build/Buildables.java
new file mode 100644
index 0000000..b0f11f6
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/Buildables.java
@@ -0,0 +1,94 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import java.lang.reflect.Modifier;
+import java.lang.reflect.TypeVariable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import org.apache.commons.lang3.reflect.MethodUtils;
+
+import software.amazon.awssdk.utils.builder.Buildable;
+
+/**
+ * Work with AWS SDK {@link Buildable}s.
+ */
+public class Buildables {
+    private static final String BUILDABLE_SUPPLIER_NAME = "builder";
+
+    static final String BUILD_NAME = "build";
+
+    @SuppressWarnings("rawtypes")
+    static final TypeVariable<Class<BuildableSupplier>> BUILDER = BuildableSupplier.class.getTypeParameters()[0];
+
+    @SuppressWarnings("rawtypes")
+    static final TypeVariable<Class<BuildableSupplier>> BUILT = BuildableSupplier.class.getTypeParameters()[1];
+
+    static final MethodSignature BUILD_METHOD;
+
+    private static final Map<Class<?>, Optional<BuildableSupplier<?, ?>>> BUILDABLE_SUPPLIERS =
+        Collections.synchronizedMap(new HashMap<>());
+
+    static {
+        try {
+            BUILD_METHOD = MethodSignature.of(Buildable.class.getDeclaredMethod(BUILD_NAME));
+        } catch (NoSuchMethodException | SecurityException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Find a {@link Supplier} of {@link Buildable} for the specified class.
+     * 
+     * @param <T>
+     *            supplied type
+     * @param c
+     *            {@link Class} instance for {@code T}
+     * @return {@link Optional} {@link Buildable} {@link Supplier}
+     */
+    public static <T> Optional<BuildableSupplier<?, T>> findBuildableSupplier(Class<T> c) {
+        @SuppressWarnings({ "unchecked", "rawtypes" })
+        final Optional<BuildableSupplier<?, T>> result = (Optional) BUILDABLE_SUPPLIERS.computeIfAbsent(c, k -> {
+            try {
+                return Optional.of(c.getDeclaredMethod(BUILDABLE_SUPPLIER_NAME))
+                    .filter(m -> quacksLikeABuildable(m.getReturnType()) && Modifier.isStatic(m.getModifiers()))
+                    .map(BuildableSupplier::of);
+            } catch (NoSuchMethodException | SecurityException e) {
+                return Optional.empty();
+            }
+        });
+        return result;
+    }
+
+    /**
+     * Learn whether {@code type} implements {@link Buildable#build()} by "duck
+     * typing."
+     * 
+     * @param type
+     *            to test
+     * @return {@code boolean}
+     */
+    static boolean quacksLikeABuildable(Class<?> type) {
+        return Buildable.class.isAssignableFrom(type)
+            || Optional.ofNullable(MethodUtils.getMatchingAccessibleMethod(type, BUILD_NAME))
+                .filter(m -> !Modifier.isStatic(m.getModifiers()) && !Void.TYPE.equals(m.getReturnType())).isPresent();
+    }
+}
diff --git a/src/main/org/apache/ant/s3/build/Builder.java b/src/main/org/apache/ant/s3/build/Builder.java
new file mode 100644
index 0000000..808cf54
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/Builder.java
@@ -0,0 +1,266 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import org.apache.ant.s3.Exceptions;
+import org.apache.ant.s3.S3DataType;
+import org.apache.ant.s3.build.ConfigurableSupplier.DynamicConfiguration;
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.ClassUtils.Interfaces;
+import org.apache.commons.lang3.reflect.TypeUtils;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.types.DataType;
+import org.apache.tools.ant.types.Reference;
+
+/**
+ * Support AWS SDK v2 fluent builder conventions.
+ *
+ * @param <T>
+ *            type to configure/introspect
+ */
+public class Builder<T> extends S3DataType implements DynamicConfiguration, Consumer<T>, ConfigurableSupplierFactory {
+
+    private abstract class Mutation implements Consumer<T> {
+        final Method mutator;
+
+        Mutation(Method mutator) {
+            this.mutator = mutator;
+        }
+
+        abstract Object getArg();
+
+        @Override
+        public void accept(T t) {
+            try {
+                mutator.invoke(t, getArg());
+            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+    }
+
+    private class AttributeMutation extends Mutation {
+        final Object arg;
+
+        AttributeMutation(Method mutator, Object arg) {
+            super(mutator);
+            this.arg = arg;
+        }
+
+        @Override
+        Object getArg() {
+            return arg;
+        }
+    }
+
+    private class ElementMutation extends Mutation {
+        final ConfigurableSupplier<?> configurableSupplier;
+
+        ElementMutation(Method mutator, ConfigurableSupplier<?> configurableSupplier) {
+            super(mutator);
+            this.configurableSupplier = configurableSupplier;
+        }
+
+        @Override
+        Object getArg() {
+            return Optional.of(getConfig()).filter(DataType.class::isInstance).map(DataType.class::cast)
+                .filter(DataType::isReference).map(DataType::getRefid).map(Reference::getReferencedObject)
+                .orElseGet(configurableSupplier);
+        }
+
+        DynamicConfiguration getConfig() {
+            return configurableSupplier.getConfiguration();
+        }
+    }
+
+    private static final TypeVariable<?> CONSUMER_ARG = Consumer.class.getTypeParameters()[0];
+
+    private static final Set<BiPredicate<String, String>> NAME_COMPARISONS =
+        Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(String::equals, String::equalsIgnoreCase)));
+
+    private static boolean isEquivalentTo(Class<?> c, Type t) {
+        if (ParameterizedType.class.isInstance(t)) {
+            t = ((ParameterizedType) t).getRawType();
+        }
+        return c.equals(t);
+    }
+
+    private static boolean isFluent(Method m) {
+        final Class<?> declaringClass = m.getDeclaringClass();
+
+        final Type genericReturnType = m.getGenericReturnType();
+
+        if (isEquivalentTo(declaringClass, genericReturnType)) {
+            return true;
+        }
+        if (TypeVariable.class.isInstance(genericReturnType)) {
+            final TypeVariable<?> var = (TypeVariable<?>) genericReturnType;
+            for (final Type bound : var.getBounds()) {
+                if (isEquivalentTo(declaringClass, bound)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private static boolean isFluentSdkMutator(Method m) {
+        return isFluent(m) && !Modifier.isStatic(m.getModifiers()) && m.getParameterTypes().length == 1;
+    }
+
+    private static <T> Builder<T>.ElementMutation elementMutation(Builder<T> b, String name) {
+        return Stream.<Function<Method, Optional<Builder<T>
+            .ElementMutation>>> of(b::cmer, b::configurableSupplier, b::fallback).map(fn -> b.searchMethods(name, fn))
+            .filter(Optional::isPresent).findFirst()
+            .orElseThrow(() -> Exceptions.create(b.buildException(), "Unknown element %s", name)).get();
+    }
+
+    protected final Class<T> target;
+    private final Set<Mutation> mutations = new LinkedHashSet<>();
+
+    /**
+     * Create a new {@link Builder} instance.
+     *
+     * @param target
+     *            type
+     * @param project
+     *            Ant {@link Project}
+     */
+    public Builder(Class<T> target, Project project) {
+        super(project);
+        this.target = Objects.requireNonNull(target, "target");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void accept(T t) {
+        validate();
+        mutations.forEach(cmer -> cmer.accept(t));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setDynamicAttribute(String uri, String localName, String qName, String value) throws BuildException {
+        final AttributeMutation mutation = searchMethods(localName, m -> {
+            try {
+                final Object convertedValue = StringConversions.as(m.getGenericParameterTypes()[0], value);
+                return Optional.of(new AttributeMutation(m, convertedValue));
+            } catch (IllegalArgumentException e) {
+                return Optional.empty();
+            }
+        }).orElseThrow(() -> Exceptions.create(buildException(), "Unknown attribute %s", localName));
+
+        mutations.add(mutation);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Object createDynamicElement(String uri, String localName, String qName) throws BuildException {
+        final ElementMutation mutation = elementMutation(this, localName);
+        mutations.add(mutation);
+        return mutation.getConfig();
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private Optional<ElementMutation> cmer(Method method) {
+        return Optional.of(method).filter(m -> Consumer.class.equals(m.getParameterTypes()[0])).map(m -> {
+            final Builder<?> builder = new Builder(TypeUtils.getRawType(
+                TypeUtils.getTypeArguments(method.getGenericParameterTypes()[0], Consumer.class).get(CONSUMER_ARG),
+                null), getProject());
+
+            return new ElementMutation(method, new ConfigurableSupplier() {
+
+                @Override
+                public Object get() {
+                    return builder;
+                }
+
+                @Override
+                public DynamicConfiguration getConfiguration() {
+                    return builder;
+                }
+            });
+        });
+    }
+
+    private Optional<ElementMutation> configurableSupplier(Method method) {
+        return configurableSupplier(method.getParameterTypes()[0]).map(cs -> new ElementMutation(method, cs));
+    }
+
+    private Optional<ElementMutation> fallback(Method m) {
+        return Optional.of(new ElementMutation(m, new ConfigurableSupplier<Object>() {
+            final Class<?> pt = m.getParameterTypes()[0];
+            final Builder<?> builder = new Builder<>(pt, getProject());
+
+            @Override
+            public Object get() {
+                throw Exceptions.create(buildExceptionTriggered(), new UnsupportedOperationException(),
+                    "Don't know how to handle argument of type %s; consider @refid for this argument", pt.getName());
+            }
+
+            @Override
+            public DynamicConfiguration getConfiguration() {
+                return builder;
+            }
+        }));
+    }
+
+    private <R> Optional<R> searchMethods(String name, Function<Method, Optional<R>> fn) {
+        for (BiPredicate<String, String> nameComparison : NAME_COMPARISONS) {
+            for (Class<?> type : ClassUtils.hierarchy(target, Interfaces.INCLUDE)) {
+                for (Method m : type.getDeclaredMethods()) {
+                    if (nameComparison.test(name, m.getName()) && isFluentSdkMutator(m)) {
+                        final Optional<R> result = fn.apply(m);
+                        if (result.isPresent()) {
+                            return result;
+                        }
+                    }
+                }
+            }
+        }
+        return Optional.empty();
+    }
+
+    private void validate() {
+        Exceptions.raiseIf(isReference() && !mutations.isEmpty(), buildException(),
+            "Cannot specify @refid in conjunction with configured builder mutations");
+    }
+}
diff --git a/src/main/org/apache/ant/s3/build/ClassFinder.java b/src/main/org/apache/ant/s3/build/ClassFinder.java
new file mode 100644
index 0000000..62576b2
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/ClassFinder.java
@@ -0,0 +1,138 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import static software.amazon.awssdk.utils.FunctionalUtils.safeFunction;
+
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.ant.s3.Exceptions;
+import org.apache.ant.s3.strings.ClassNames;
+import org.apache.ant.s3.strings.PackageNames;
+import org.apache.ant.s3.strings.Strings;
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Entity to find Java classes by naming convention. A "fragment" is "plugged
+ * into" a matrix of packagename prefixes and classname suffixes to yield FQ
+ * classnames. The fragment can be prefixed by a full or partial package name,
+ * and is also interpretable as a FQ classname itself.
+ */
+public class ClassFinder {
+
+    /**
+     * {@link Pattern} to identify the point at which the final dot in a
+     * {@link String} is followed by a lowercase character.
+     */
+    private static final Pattern PACKAGE_TO_CLASS_TRANSITION = Pattern.compile("((?:^|\\.)[a-z])(?=[^\\.]*$)");
+
+    private static String toClassConvention(String fragment) {
+        final String result;
+        final Matcher m = PACKAGE_TO_CLASS_TRANSITION.matcher(fragment);
+        if (m.find()) {
+            final StringBuffer b = new StringBuffer();
+            m.appendReplacement(b, m.group(1).toUpperCase());
+            m.appendTail(b);
+            result = b.toString();
+        } else {
+            result = fragment;
+        }
+        return StringUtils.stripStart(result, ".");
+    }
+
+    private static String toString(Strings strings) {
+        return strings.stream().collect(Collectors.joining(", ", "[", "]"));
+    }
+
+    private final PackageNames packageNames;
+    private final ClassNames suffixes;
+
+    /**
+     * Create a new {@link ClassFinder}.
+     * 
+     * @param packageNames
+     *            relative to which to search
+     * @param suffixes
+     *            to append
+     */
+    public ClassFinder(Iterable<String> packageNames, Iterable<String> suffixes) {
+        this.packageNames = PackageNames.of("").andThen(packageNames).distinct();
+        this.suffixes = ClassNames.of("").andThen(suffixes).distinct();
+    }
+
+    /**
+     * Find a {@link Class} instance plugging the specified shorthand
+     * {@link String} into the specified matrix of package names and classname
+     * suffixes.
+     * 
+     * @param s
+     *            fragment whose first alpha character after any final dot
+     *            ({@code .}) will be converted to uppercase
+     * @return {@link Class}
+     */
+    public Class<?> find(String s) {
+        return find(s, null);
+    }
+
+    /**
+     * Find a {@link Class} instance plugging the specified shorthand
+     * {@link String} into the specified matrix of package names and classname
+     * suffixes.
+     * 
+     * @param <T>
+     *            supertype
+     * @param s
+     *            fragment whose first alpha character after any final dot
+     *            ({@code .}) will be converted to uppercase
+     * @param requiredSupertype
+     *            may be {@code null}
+     * @return {@link Class} extending {@code T}
+     */
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public <T> Class<? extends T> find(String s, Class<T> requiredSupertype) {
+        final String fragment = toClassConvention(s);
+
+        final Predicate<Class<?>> filter;
+        final ClassLoader loader;
+        if (requiredSupertype == null) {
+            filter = c -> true;
+            loader = Thread.currentThread().getContextClassLoader();
+        } else {
+            filter = requiredSupertype::isAssignableFrom;
+            loader = requiredSupertype.getClassLoader();
+        }
+        return (Class) packageNames.stream().map(p -> p.isEmpty() ? fragment : p + '.' + fragment)
+            .flatMap(pf -> suffixes.stream().map(sf -> pf + sf)).map(name -> probeFor(loader, name))
+            .filter(Objects::nonNull).filter(filter).findFirst()
+            .orElseThrow(() -> Exceptions.create(IllegalArgumentException::new,
+                () -> String.format("Cannot find class for '%s' among packages %s X suffixes %s", s,
+                    toString(packageNames), toString(suffixes))));
+    }
+
+    private Class<?> probeFor(ClassLoader loader, String className) {
+        try {
+            return ClassUtils.getClass(loader, className);
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/org/apache/ant/s3/build/ConfigurableSupplier.java b/src/main/org/apache/ant/s3/build/ConfigurableSupplier.java
new file mode 100644
index 0000000..8a90e73
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/ConfigurableSupplier.java
@@ -0,0 +1,77 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import java.util.function.Supplier;
+
+import org.apache.ant.s3.Exceptions;
+import org.apache.ant.s3.ProjectUtils;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.DynamicAttributeNS;
+import org.apache.tools.ant.DynamicElementNS;
+
+/**
+ * A configurable {@link Supplier}.
+ *
+ * @param <T>
+ *            supplied type
+ */
+public interface ConfigurableSupplier<T> extends Supplier<T> {
+
+    /**
+     * Dynamic configuration superset interface.
+     */
+    interface DynamicConfiguration extends DynamicAttributeNS, DynamicElementNS, ProjectUtils {
+
+        /**
+         * {@inheritDoc}
+         * 
+         * Default implementation.
+         * 
+         * @throws BuildException
+         *             always
+         */
+        @Override
+        default void setDynamicAttribute(String uri, String localName, String qName, String value)
+            throws BuildException {
+            Exceptions.raise(buildExceptionTriggered(), new UnsupportedOperationException(), "@%s not supported",
+                qName);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * Default implementation.
+         * 
+         * @throws BuildException
+         *             always
+         */
+        @Override
+        default Object createDynamicElement(String uri, String localName, String qName) throws BuildException {
+            throw Exceptions.create(buildExceptionTriggered(), new UnsupportedOperationException(),
+                "nested %s element not supported", qName);
+        }
+    }
+
+    /**
+     * Get the {@link DynamicConfiguration} for this
+     * {@link ConfigurableSupplier}.
+     * 
+     * @return {@link DynamicConfiguration}
+     */
+    DynamicConfiguration getConfiguration();
+}
diff --git a/src/main/org/apache/ant/s3/build/ConfigurableSupplierFactory.java b/src/main/org/apache/ant/s3/build/ConfigurableSupplierFactory.java
new file mode 100644
index 0000000..159ac39
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/ConfigurableSupplierFactory.java
@@ -0,0 +1,76 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import java.util.Optional;
+
+import org.apache.tools.ant.Project;
+
+import software.amazon.awssdk.utils.builder.Buildable;
+
+/**
+ * Mixin style implementation of functionality to create
+ * {@link ConfigurableSupplier}s, permitting creation of subordinate objects
+ * bound to a single Ant {@link Project}.
+ */
+public interface ConfigurableSupplierFactory {
+    /**
+     * Primary functionality provided by this interface. Get a
+     * {@link ConfigurableSupplier} for the specified {@link Class}.
+     * 
+     * @param <T> type param
+     * @param type supplied
+     * @return {@link Optional} {@link ConfigurableSupplier} of {@code T}
+     */
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    default <T> Optional<ConfigurableSupplier<T>> configurableSupplier(Class<T> type) {
+        final Optional<ConfiguringSupplier<T>> configuringSupplier =
+            ConfiguringSuppliers.forProject(getProject()).findConfiguringSupplier(type);
+
+        if (configuringSupplier.isPresent()) {
+            return (Optional) configuringSupplier;
+        }
+        final Optional<BuildableSupplier<?, T>> buildableSupplier = Buildables.findBuildableSupplier(type);
+        if (buildableSupplier.isPresent()) {
+            final Buildable buildable = buildableSupplier.get().get();
+            final Builder builder = new Builder(buildable.getClass(), getProject());
+
+            return Optional.of(new ConfigurableSupplier<T>() {
+
+                @Override
+                public T get() {
+                    builder.accept(buildable);
+                    return (T) buildable.build();
+                }
+
+                @Override
+                public DynamicConfiguration getConfiguration() {
+                    return builder;
+                }
+            });
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * Get the {@link Project} associated with this
+     * {@link ConfigurableSupplierFactory}.
+     * 
+     * @return {@link Project}
+     */
+    Project getProject();
+}
diff --git a/src/main/org/apache/ant/s3/build/ConfiguringSupplier.java b/src/main/org/apache/ant/s3/build/ConfiguringSupplier.java
new file mode 100644
index 0000000..981f920
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/ConfiguringSupplier.java
@@ -0,0 +1,39 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import org.apache.ant.s3.build.ConfigurableSupplier.DynamicConfiguration;
+
+/**
+ * A {@link ConfigurableSupplier} that <em>is</em> its own
+ * {@code configuration}.
+ * 
+ * @param <T>
+ *            supplied type
+ */
+public interface ConfiguringSupplier<T> extends DynamicConfiguration, ConfigurableSupplier<T> {
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @return {@code this}
+     */
+    @Override
+    default DynamicConfiguration getConfiguration() {
+        return this;
+    }
+}
diff --git a/src/main/org/apache/ant/s3/build/ConfiguringSuppliers.java b/src/main/org/apache/ant/s3/build/ConfiguringSuppliers.java
new file mode 100644
index 0000000..90d2b57
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/ConfiguringSuppliers.java
@@ -0,0 +1,96 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.apache.ant.s3.build.spi.ConfiguringSuppliersProvider;
+import org.apache.ant.s3.build.spi.Providers;
+import org.apache.tools.ant.Project;
+
+/**
+ * {@link ConfiguringSupplier}s management.
+ */
+public class ConfiguringSuppliers {
+    private static final Map<Class<?>, Optional<Function<Project, ConfiguringSupplier<?>>>> CONFIGURING_SUPPLIERS =
+        Collections.synchronizedMap(new HashMap<>());
+
+    static {
+        Providers.stream(ConfiguringSuppliersProvider.class).map(ConfiguringSuppliersProvider::get)
+            .forEach(ConfiguringSuppliers::registerConfiguringSuppliers);
+    }
+
+    /**
+     * Get the {@link ConfiguringSuppliers} instance for the specified
+     * {@link Project}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link ConfiguringSuppliers}
+     */
+    public static ConfiguringSuppliers forProject(Project project) {
+        ConfiguringSuppliers result = project.getReference(ConfiguringSuppliers.class.getName());
+        if (result == null) {
+            result = new ConfiguringSuppliers(project);
+            project.addReference(ConfiguringSuppliers.class.getName(), result);
+        }
+        return result;
+    }
+
+    /**
+     * Register the {@link ConfiguringSupplier} {@link Supplier}s in the
+     * specified {@link Map}.
+     * 
+     * @param configuringSuppliers
+     *            to register
+     * @return whether any existing {@link ConfiguringSupplier} {@link Supplier}
+     *         was displaced by this process
+     */
+    static boolean registerConfiguringSuppliers(
+        Map<Class<?>, Function<Project, ConfiguringSupplier<?>>> configuringSuppliers) {
+        return configuringSuppliers.entrySet().stream()
+            .map(e -> CONFIGURING_SUPPLIERS.put(e.getKey(), Optional.of(e.getValue()))).filter(Objects::nonNull)
+            .anyMatch(Optional::isPresent);
+    }
+
+    private final Project project;
+
+    private ConfiguringSuppliers(Project project) {
+        this.project = Objects.requireNonNull(project);
+    }
+
+    /**
+     * Find a {@link ConfiguringSupplier} for the specified class.
+     * 
+     * @param <T>
+     *            supplied type
+     * @param c
+     *            type instance
+     * @return {@link Optional} {@link ConfiguringSupplier}
+     */
+    @SuppressWarnings("unchecked")
+    public <T> Optional<ConfiguringSupplier<T>> findConfiguringSupplier(Class<T> c) {
+        return CONFIGURING_SUPPLIERS.computeIfAbsent(c, k -> Optional.empty())
+            .map(fn -> (ConfiguringSupplier<T>) fn.apply(project));
+    }
+}
diff --git a/src/main/org/apache/ant/s3/build/DefaultStringConversionsProvider.java b/src/main/org/apache/ant/s3/build/DefaultStringConversionsProvider.java
new file mode 100644
index 0000000..e598b8f
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/DefaultStringConversionsProvider.java
@@ -0,0 +1,116 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.function.Function;
+
+import org.apache.ant.s3.build.spi.DefaultProvider;
+import org.apache.ant.s3.build.spi.StringConversionsProvider;
+import org.kohsuke.MetaInfServices;
+
+/**
+ * Default/baseline {@link StringConversionsProvider}.
+ */
+@DefaultProvider
+@MetaInfServices
+public class DefaultStringConversionsProvider extends StringConversionsProvider {
+    /**
+     * {@link Byte} from {@link String}.
+     */
+    public final Function<String, Byte> toByte = Byte::valueOf;
+
+    /**
+     * {@link Short} from {@link String}.
+     */
+    public final Function<String, Short> toShort = Short::valueOf;
+
+    /**
+     * {@link Integer} from {@link String}.
+     */
+    public final Function<String, Integer> toInt = Integer::valueOf;
+
+    /**
+     * {@link Character} from {@link String}.
+     */
+    public final Function<String, Character> toChar = s -> s.charAt(0);
+
+    /**
+     * {@link Long} from {@link String}.
+     */
+    public final Function<String, Long> toLong = Long::valueOf;
+
+    /**
+     * {@link Float} from {@link String}.
+     */
+    public final Function<String, Float> toFloat = Float::valueOf;
+
+    /**
+     * {@link Double} from {@link String}.
+     */
+    public final Function<String, Double> toDouble = Double::valueOf;
+
+    /**
+     * {@link Boolean} from {@link String}.
+     */
+    public final Function<String, Boolean> toBoolean = Boolean::valueOf;
+
+    /**
+     * Identity.
+     */
+    public final Function<String, String> toString = Function.identity();
+
+    /**
+     * {@link Duration} from {@link String}.
+     */
+    public final Function<String, Duration> toDuration = Duration::parse;
+
+    /**
+     * {@link URI} from {@link String}.
+     */
+    public final Function<String, URI> toUri = URI::create;
+
+    /**
+     * {@link Path} from {@link String}.
+     */
+    public final Function<String, Path> toPath = Paths::get;
+
+    /**
+     * {@link BigDecimal} from {@link String}.
+     */
+    public final Function<String, BigDecimal> toBigDecimal = BigDecimal::new;
+
+    /**
+     * {@link BigInteger} from {@link String}.
+     */
+    public final Function<String, BigInteger> toBigInteger = BigInteger::new;
+
+    /**
+     * {@code byte[]} from {@link String}.
+     */
+    public final Function<String, byte[]> toByteArray = String::getBytes;
+
+    /**
+     * {@code char[]} from {@link String}.
+     */
+    public final Function<String, char[]> toCharArray = String::toCharArray;
+}
diff --git a/src/main/org/apache/ant/s3/build/MetaBuilderByType.java b/src/main/org/apache/ant/s3/build/MetaBuilderByType.java
new file mode 100644
index 0000000..af23ff8
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/MetaBuilderByType.java
@@ -0,0 +1,140 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+
+import org.apache.ant.s3.Exceptions;
+import org.apache.ant.s3.ProjectUtils;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.ProjectComponent;
+
+import software.amazon.awssdk.utils.builder.Buildable;
+
+/**
+ * {@link ConfiguringSupplier} by named Java type.
+ */
+public class MetaBuilderByType<T> extends ProjectComponent
+    implements ConfiguringSupplier<T>, ConfigurableSupplierFactory, ProjectUtils {
+
+    private final Class<T> api;
+    private final ClassFinder classFinder;
+    private final Class<? extends T> defaultImpl;
+    private Map<String, String> attributeCache = new LinkedHashMap<>();
+    private BiConsumer<String, String> attributeCmer = attributeCache::put;
+    private Optional<ConfigurableSupplier<? extends T>> configurableSupplier = Optional.empty();
+
+    /**
+     * Create a new {@link MetaBuilderByType}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @param api
+     *            supertype whose child to find
+     * @param classFinder
+     *            to use
+     */
+    public MetaBuilderByType(Project project, Class<T> api, ClassFinder classFinder) {
+        this(project, api, classFinder, null);
+    }
+
+    /**
+     * Create a new {@link MetaBuilderByType}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @param api
+     *            supertype whose child to find
+     * @param classFinder
+     *            to use
+     * @param defaultImpl
+     *            of {@code api}
+     */
+    public MetaBuilderByType(Project project, Class<T> api, ClassFinder classFinder, Class<? extends T> defaultImpl) {
+        this.api = api;
+        this.classFinder = classFinder;
+        this.defaultImpl = defaultImpl;
+        setProject(project);
+    }
+
+    /**
+     * Set the implementation type by name.
+     * 
+     * @param impl
+     *            fragment
+     * @see ClassFinder#find(String, Class)
+     */
+    public void setImpl(String impl) {
+        useImpl(classFinder.find(impl, api));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public T get() {
+        return configurableSupplier().get();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public synchronized void setDynamicAttribute(String uri, String localName, String qName, String value)
+        throws BuildException {
+        attributeCmer.accept(localName, value);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Object createDynamicElement(String uri, String localName, String qName) throws BuildException {
+        return configurableSupplier().getConfiguration().createDynamicElement(uri, localName, qName);
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private void useImpl(Class<? extends T> type) {
+        Exceptions.raiseIf(configurableSupplier.isPresent(), buildExceptionTriggered(), new IllegalStateException(),
+            "Already set %s [for type %s]", ConfigurableSupplier.class.getSimpleName());
+
+        configurableSupplier = (Optional) configurableSupplier(type);
+
+        Exceptions.raiseUnless(configurableSupplier.isPresent(), buildExceptionTriggered(),
+            new IllegalArgumentException(), "Could not find %s for %s", Buildable.class.getSimpleName(), type);
+
+        synchronized (this) {
+            attributeCmer = (k, v) -> configurableSupplier().getConfiguration().setDynamicAttribute(null, k, null, v);
+            attributeCache.forEach(attributeCmer);
+            attributeCache = null;
+        }
+    }
+
+    private ConfigurableSupplier<? extends T> configurableSupplier() {
+        if (!configurableSupplier.isPresent()) {
+            Exceptions.raiseIf(defaultImpl == null, buildExceptionTriggered(), new IllegalStateException(),
+                "subtype has not been set/found and no default impl was configured");
+
+            useImpl(defaultImpl);
+        }
+        return configurableSupplier.get();
+    }
+}
diff --git a/src/main/org/apache/ant/s3/build/MethodSignature.java b/src/main/org/apache/ant/s3/build/MethodSignature.java
new file mode 100644
index 0000000..c2add3a
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/MethodSignature.java
@@ -0,0 +1,126 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Modeled method signature.
+ */
+public final class MethodSignature implements Predicate<Method> {
+
+    private static final Set<MethodSignature> INTERNED = new HashSet<>();
+
+    /**
+     * Factory method.
+     * 
+     * @param m
+     *            {@link Method}
+     * @return {@link MethodSignature}
+     */
+    public static MethodSignature of(Method m) {
+        final MethodSignature result = new MethodSignature(m.getName(), m.getParameterTypes());
+
+        final Optional<MethodSignature> interned = INTERNED.stream().filter(Predicate.isEqual(result)).findFirst();
+
+        if (interned.isPresent()) {
+            return interned.get();
+        }
+        INTERNED.add(result);
+        return result;
+    }
+
+    private final String name;
+    private final List<Class<?>> parameterTypes;
+
+    private MethodSignature(String name, Class<?>[] parameterTypes) {
+        this.name = name;
+        this.parameterTypes = Stream.of(parameterTypes)
+            .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
+    }
+
+    /**
+     * Get the {@code name}.
+     * 
+     * @return {@link String}
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Get the parameter types.
+     * 
+     * @return {@link List} of {@link Class}
+     */
+    public List<Class<?>> getParameterTypes() {
+        return parameterTypes;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, parameterTypes);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof MethodSignature)) {
+            return false;
+        }
+        final MethodSignature other = (MethodSignature) obj;
+        return Objects.equals(name, other.name) && Objects.equals(parameterTypes, other.parameterTypes);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return parameterTypes.stream().map(Class::getName).collect(Collectors.joining(", ", name + "(", ")"));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @param t
+     *            {@link Method}
+     * @return {@code boolean}
+     */
+    @Override
+    public boolean test(Method t) {
+        return name.equals(t.getName()) && Arrays.asList(t.getParameterTypes()).equals(parameterTypes);
+    }
+}
\ No newline at end of file
diff --git a/src/main/org/apache/ant/s3/build/RootConfiguringSupplier.java b/src/main/org/apache/ant/s3/build/RootConfiguringSupplier.java
new file mode 100644
index 0000000..4326996
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/RootConfiguringSupplier.java
@@ -0,0 +1,116 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import org.apache.ant.s3.Exceptions;
+import org.apache.ant.s3.S3DataType;
+import org.apache.commons.lang3.reflect.TypeUtils;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+
+import software.amazon.awssdk.utils.Lazy;
+
+/**
+ * Root {@link ConfiguringSupplier}.
+ *
+ * @param <T>
+ *            supplied type
+ */
+public class RootConfiguringSupplier<T> extends S3DataType
+    implements ConfiguringSupplier<T>, ConfigurableSupplierFactory {
+
+    private static final TypeVariable<?> SUPPLIED_TYPE = Supplier.class.getTypeParameters()[0];
+
+    private final ConfigurableSupplier<T> configurableSupplier;
+    private final Lazy<T> payload;
+
+    /**
+     * Create a new {@link RootConfiguringSupplier}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @param t
+     *            type supplied
+     */
+    @SuppressWarnings("unchecked")
+    public RootConfiguringSupplier(Project project, Class<T> t) {
+        super(project);
+
+        if (t == null) {
+            final Type boundType = TypeUtils.getTypeArguments(getClass(), Supplier.class).get(SUPPLIED_TYPE);
+
+            Exceptions.raiseIf(boundType == null, IllegalStateException::new, "%s does not bind %s", getClass(),
+                SUPPLIED_TYPE);
+
+            t = (Class<T>) TypeUtils.getRawType(boundType, null);
+        }
+        configurableSupplier = configurableSupplier(t).get();
+        payload = new Lazy<>(configurableSupplier::get);
+    }
+
+    /**
+     * Create a new {@link RootConfiguringSupplier} for a bound subtype.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     */
+    protected RootConfiguringSupplier(Project project) {
+        this(project, null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public T get() {
+        return payload.getValue();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setDynamicAttribute(String uri, String localName, String qName, String value) throws BuildException {
+        configurableSupplier.getConfiguration().setDynamicAttribute(uri, localName, qName, value);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Object createDynamicElement(String uri, String localName, String qName) throws BuildException {
+        return configurableSupplier.getConfiguration().createDynamicElement(uri, localName, qName);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public <TT> Optional<ConfigurableSupplier<TT>> configurableSupplier(Class<TT> type) {
+        final Optional<ConfigurableSupplier<TT>> result = ConfigurableSupplierFactory.super.configurableSupplier(type);
+
+        Exceptions.raiseUnless(result.isPresent(), buildExceptionTriggered(), new IllegalArgumentException(),
+            "Could not find %s for %s", ConfigurableSupplier.class.getSimpleName(), type);
+
+        return result;
+    }
+}
diff --git a/src/main/org/apache/ant/s3/build/StringConversions.java b/src/main/org/apache/ant/s3/build/StringConversions.java
new file mode 100644
index 0000000..c52cb0d
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/StringConversions.java
@@ -0,0 +1,248 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.ant.s3.Exceptions;
+import org.apache.ant.s3.build.spi.Providers;
+import org.apache.ant.s3.build.spi.StringConversionsProvider;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.reflect.TypeUtils;
+import org.apache.commons.lang3.reflect.Typed;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+
+import software.amazon.awssdk.utils.AttributeMap;
+
+/**
+ * Static utility class for conversions from {@link String} to needed types.
+ */
+public class StringConversions {
+    private static final TypeVariable<?> ATTRIBUTE_KEY_TYPE = AttributeMap.Key.class.getTypeParameters()[0];
+
+    private static final TypeVariable<?> COLLECTION_ELEMENT = Collection.class.getTypeParameters()[0];
+
+    private static final Map<Class<?>, Function<String, ?>> CONVERTERS = new HashMap<>();
+
+    static {
+        Providers.stream(StringConversionsProvider.class).map(StringConversionsProvider::get)
+            .forEach(StringConversions::register);
+    }
+
+    private static Field keyField(Class<? extends AttributeMap.Key<?>> keyType, String name) {
+        try {
+            final Field result = keyType.getDeclaredField(name.toUpperCase(Locale.US));
+            Exceptions.raiseUnless(Modifier.isStatic(result.getModifiers()) && result.getType().equals(keyType),
+                IllegalArgumentException::new,
+                () -> String.format("Illegal %s key: %s", keyType.getSimpleName(), name));
+            return result;
+        } catch (NoSuchFieldException | SecurityException e) {
+            throw new BuildException(e);
+        }
+    }
+
+    /**
+     * Convert {@code value} to the type represented by {@code type}.
+     *
+     * @param <T>
+     *            target
+     * @param type
+     *            {@link Typed} of {@code T}
+     * @param value
+     *            source
+     * @return {@code T}
+     */
+    public static <T> T as(Typed<T> type, String value) {
+        return as(type.getType(), value);
+    }
+
+    /**
+     * Convert {@code value} to {@code type}.
+     *
+     * @param <T>
+     *            target
+     * @param type
+     *            target type instance
+     * @param value
+     *            source
+     * @return {@code T}
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T as(Type type, String value) {
+        final Class<?> clazz = ClassUtils.primitiveToWrapper(TypeUtils.getRawType(type, null));
+
+        final Optional<Object> converted = Optional.of(clazz).map(CONVERTERS::get).map(fn -> fn.apply(value));
+
+        if (converted.isPresent()) {
+            return (T) converted.get();
+        }
+        final Optional<Type> componentType = getComponentType(type);
+        if (componentType.isPresent()) {
+            final List<Object> list = Stream.of(StringUtils.split(value, ',')).map(String::trim)
+                .map(v -> as(componentType.get(), v)).collect(Collectors.toList());
+
+            return (T) toCollectionish(type, list);
+        }
+        if (clazz.isEnum()) {
+            try {
+                @SuppressWarnings("rawtypes")
+                final T result = (T) Enum.valueOf((Class) clazz, value);
+                return result;
+            } catch (IllegalArgumentException e) {
+            }
+            @SuppressWarnings("rawtypes")
+            final T result = (T) Enum.valueOf((Class) clazz, value.toUpperCase(Locale.US));
+            return result;
+        }
+        // Ant conventions
+        Constructor<T> ctor;
+        try {
+            final Constructor<T> _ctor = (Constructor<T>) clazz.getDeclaredConstructor(Project.class, String.class);
+            ctor = _ctor;
+        } catch (NoSuchMethodException | SecurityException e) {
+            try {
+                final Constructor<T> _ctor = (Constructor<T>) clazz.getDeclaredConstructor(String.class);
+                ctor = _ctor;
+            } catch (NoSuchMethodException | SecurityException e2) {
+                ctor = null;
+            }
+        }
+        if (ctor != null) {
+            try {
+                return ctor.newInstance(value);
+            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+        throw new IllegalArgumentException();
+    }
+
+    /**
+     * Generate an AWS SDK {@link AttributeMap} from the specified
+     * {@link String} {@link Map} for the specified key type.
+     *
+     * @param <K>
+     *            key type
+     * @param keyType
+     *            as {@link Class}
+     * @param m
+     *            source
+     * @return {@link AttributeMap}
+     */
+    public static <K extends AttributeMap.Key<?>> AttributeMap attributes(Class<K> keyType, Map<String, String> m) {
+        final AttributeMap.Builder b = AttributeMap.builder();
+
+        m.forEach((k, v) -> {
+            final Field keyField = keyField(keyType, k);
+            final AttributeMap.Key<Object> key;
+            try {
+                @SuppressWarnings("unchecked")
+                final AttributeMap.Key<Object> _key = (AttributeMap.Key<Object>) keyField.get(null);
+                key = _key;
+            } catch (IllegalArgumentException | IllegalAccessException e) {
+                throw new BuildException(e);
+            }
+            final Type valueType =
+                TypeUtils.getTypeArguments(keyField.getGenericType(), AttributeMap.Key.class).get(ATTRIBUTE_KEY_TYPE);
+
+            final Object value = as(valueType, v);
+
+            b.<Object> put(key, value);
+        });
+
+        return b.build();
+    }
+
+    /**
+     * Register the converters in the specified {@link Map}.
+     * 
+     * @param converters
+     *            to register
+     * @return whether any existing converter was displaced by this process
+     */
+    static boolean register(Map<Class<?>, Function<String, ?>> converters) {
+        return converters.entrySet().stream().map(e -> CONVERTERS.put(e.getKey(), e.getValue()))
+            .anyMatch(Objects::nonNull);
+    }
+
+    private static Optional<Type> getComponentType(Type t) {
+        final Class<?> clazz = TypeUtils.getRawType(t, null);
+        if (clazz.isArray()) {
+            return Optional.of(clazz.getComponentType());
+        }
+        if (Collection.class.isAssignableFrom(clazz)) {
+            return Optional.ofNullable(TypeUtils.getTypeArguments(t, Collection.class))
+                .map(m -> m.get(COLLECTION_ELEMENT));
+        }
+        return Optional.empty();
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private static <T> T toCollectionish(Type t, List<Object> l) {
+        final Class<?> c = TypeUtils.getRawType(t, null);
+        if (Arrays.asList(Collection.class, List.class).contains(c)) {
+            return (T) l;
+        }
+        if (Set.class.equals(c)) {
+            if (Optional.of(COLLECTION_ELEMENT).map(TypeUtils.getTypeArguments(t, Collection.class)::get)
+                .filter(Class.class::isInstance).map(Class.class::cast).filter(Class::isEnum).isPresent()) {
+                return (T) EnumSet.copyOf((List) l);
+            }
+            return (T) new LinkedHashSet<>(l);
+        }
+        if (Collection.class.isAssignableFrom(c)) {
+            try {
+                final Constructor<?> ctor = c.getDeclaredConstructor(Collection.class);
+                return (T) ctor.newInstance(l);
+            } catch (Exception e) {
+            }
+        }
+        if (c.isArray()) {
+            Class<?> componentType = c.getComponentType();
+            final boolean primitive = (componentType.isPrimitive());
+            if (primitive) {
+                componentType = ClassUtils.primitiveToWrapper(componentType);
+            }
+            final Object result = l.toArray((Object[]) Array.newInstance(componentType, l.size()));
+            return (T) (primitive ? ArrayUtils.toPrimitive(result) : result);
+        }
+        throw new IllegalArgumentException();
+    }
+}
diff --git a/src/main/org/apache/ant/s3/build/spi/ConfiguringSuppliersProvider.java b/src/main/org/apache/ant/s3/build/spi/ConfiguringSuppliersProvider.java
new file mode 100644
index 0000000..da636cd
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/spi/ConfiguringSuppliersProvider.java
@@ -0,0 +1,76 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build.spi;
+
+import static software.amazon.awssdk.utils.FunctionalUtils.safeFunction;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.apache.ant.s3.build.ConfiguringSupplier;
+import org.apache.commons.lang3.reflect.TypeUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.tools.ant.Project;
+
+/**
+ * {@code abstract} SPI class for {@link ConfiguringSupplier}s provision.
+ * Methods accepting a {@link Project} and returning a
+ * {@link ConfiguringSupplier} are converted to {@link Function}s and mapped to
+ * the supplied type.
+ */
+public abstract class ConfiguringSuppliersProvider
+    extends IntrospectingProviderBase<Map.Entry<Class<?>, Function<Project, ConfiguringSupplier<?>>>>
+    implements Supplier<Map<Class<?>, Function<Project, ConfiguringSupplier<?>>>> {
+
+    private static final TypeVariable<?> SUPPLIED_TYPE = Supplier.class.getTypeParameters()[0];
+
+    /**
+     * Create a new {@link ConfiguringSuppliersProvider}.
+     */
+    public ConfiguringSuppliersProvider() {
+        fieldFilter(filter -> f -> false);
+        methodFilter(filter -> filter.and(args(Project.class)).and(returns(ConfiguringSupplier.class)));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Map<Class<?>, Function<Project, ConfiguringSupplier<?>>> get() {
+        final Map<Class<?>, Function<Project, ConfiguringSupplier<?>>> result = new LinkedHashMap<>();
+
+        introspect(e -> result.put(e.getKey(), e.getValue()));
+
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Optional<Map.Entry<Class<?>, Function<Project, ConfiguringSupplier<?>>>> map(Method m) {
+        final Type supplied = TypeUtils.getTypeArguments(m.getGenericReturnType(), Supplier.class).get(SUPPLIED_TYPE);
+        return Optional.of(Pair.of(TypeUtils.getRawType(supplied, null),
+            safeFunction(p -> (ConfiguringSupplier<?>) m.invoke(this, p))));
+    }
+}
diff --git a/src/main/org/apache/ant/s3/build/spi/DefaultProvider.java b/src/main/org/apache/ant/s3/build/spi/DefaultProvider.java
new file mode 100644
index 0000000..ff5be8b
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/spi/DefaultProvider.java
@@ -0,0 +1,33 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build.spi;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation for service provider implementations that should be installed
+ * earliest, in case another available implementation has reason to override their effects. 
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DefaultProvider {
+}
diff --git a/src/main/org/apache/ant/s3/build/spi/IntrospectingProviderBase.java b/src/main/org/apache/ant/s3/build/spi/IntrospectingProviderBase.java
new file mode 100644
index 0000000..26f1e3b
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/spi/IntrospectingProviderBase.java
@@ -0,0 +1,183 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build.spi;
+
+import static java.lang.reflect.Modifier.FINAL;
+import static java.lang.reflect.Modifier.PUBLIC;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.LinkedHashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.apache.ant.s3.build.MethodSignature;
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.commons.lang3.reflect.TypeUtils;
+
+/**
+ * Base class for introspecting SPI providers.
+ */
+public abstract class IntrospectingProviderBase<E> {
+
+    private Predicate<Field> fieldFilter = mods(PUBLIC | FINAL);
+    private Predicate<Method> methodFilter = uniqueSignatures().and(mods(PUBLIC));
+
+    /**
+     * Override or augment the field filter {@link Predicate}.
+     * 
+     * @param mod
+     *            accepts existing filter
+     */
+    protected void fieldFilter(UnaryOperator<Predicate<Field>> mod) {
+        this.fieldFilter = mod.apply(fieldFilter);
+    }
+
+    /**
+     * Override or augment the method filter {@link Predicate}.
+     * 
+     * @param mod
+     *            accepts existing filter
+     */
+    protected void methodFilter(UnaryOperator<Predicate<Method>> mod) {
+        this.methodFilter = mod.apply(methodFilter);
+    }
+
+    /**
+     * Introspect this object's fields and methods, mapping and sending to the
+     * specified {@link Consumer}.
+     * 
+     * @param cmer
+     *            accepts mapped objects
+     * @see #map(Field)
+     * @see #map(Method)
+     */
+    protected void introspect(Consumer<E> cmer) {
+        final Stream<Optional<E>> fromFields =
+            Stream.of(FieldUtils.getAllFields(getClass())).filter(fieldFilter).map(this::map);
+
+        final Stream<Optional<E>> fromMethods =
+            StreamSupport.stream(ClassUtils.hierarchy(getClass()).spliterator(), false).map(Class::getDeclaredMethods)
+                .flatMap(Stream::of).filter(methodFilter).map(this::map);
+
+        Stream.concat(fromFields, fromMethods).filter(Optional::isPresent).map(Optional::get).forEach(cmer);
+    }
+
+    /**
+     * Map a {@link Field} that passed the {@link #fieldFilter} to {@code E}.
+     * 
+     * @param f
+     *            {@link Field} to map
+     * @return {@link Optional} of {@code E}
+     */
+    protected Optional<E> map(Field f) {
+        return Optional.empty();
+    }
+
+    /**
+     * Map a {@link Method} that passed the {@link #methodFilter} to {@code E}.
+     * 
+     * @param m
+     *            {@link Method} to map
+     * @return {@link Optional} of {@code E}
+     */
+    protected Optional<E> map(Method m) {
+        return Optional.empty();
+    }
+
+    /**
+     * Create a {@link Predicate} to select {@link Member}s by present Java
+     * modifiers.
+     * 
+     * @param <M>
+     *            {@link Member} type
+     * @param mods
+     *            mask
+     * @return {@link Predicate} of {@code <M>}
+     */
+    protected <M extends Member> Predicate<M> mods(int mods) {
+        return m -> (m.getModifiers() & mods) == mods;
+    }
+
+    /**
+     * Create a {@link Predicate} to select {@link Member}s by name.
+     * 
+     * @param <M>
+     *            {@link Member} type
+     * @param test
+     *            name {@link Predicate}
+     * @return {@link Predicate} of {@code <M>}
+     */
+    protected <M extends Member> Predicate<M> named(Predicate<String> test) {
+        return m -> test.test(m.getName());
+    }
+
+    /**
+     * Create a {@link Predicate} to select {@link Field}s by declared type
+     * assignability.
+     * 
+     * @param t
+     *            compare to {@link Field#getGenericType()}
+     * @return {@link Predicate} of {@link Field}
+     */
+    protected Predicate<Field> type(Type t) {
+        return f -> TypeUtils.isAssignable(f.getGenericType(), t);
+    }
+
+    /**
+     * Return a {@link Predicate} that discards a {@link Method} if its unique
+     * signature has already been processed.
+     * 
+     * @return {@link Predicate} of {@link Method}
+     */
+    protected Predicate<Method> uniqueSignatures() {
+        final Set<MethodSignature> signaturesEncountered = new LinkedHashSet<>();
+        return m -> signaturesEncountered.add(MethodSignature.of(m));
+    }
+
+    /**
+     * Return a {@link Predicate} to select {@link Method}s by argument type
+     * assignability.
+     * 
+     * @param args
+     *            compare to {@link Method#getParameterTypes()}
+     * @return {@link Predicate} of {@link Method}
+     */
+    protected Predicate<Method> args(Class<?>... args) {
+        return m -> ClassUtils.isAssignable(m.getParameterTypes(), args);
+    }
+
+    /**
+     * Return a {@link Predicate} to select {@link Method}s by return type
+     * assignability.
+     * 
+     * @param t
+     *            compare to {@link Method#getGenericReturnType()}
+     * @return {@link Predicate} of {@link Method}
+     */
+    protected Predicate<Method> returns(Type t) {
+        return m -> TypeUtils.isAssignable(m.getGenericReturnType(), t);
+    }
+}
diff --git a/src/main/org/apache/ant/s3/build/spi/Providers.java b/src/main/org/apache/ant/s3/build/spi/Providers.java
new file mode 100644
index 0000000..29259b2
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/spi/Providers.java
@@ -0,0 +1,64 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build.spi;
+
+import java.util.Comparator;
+import java.util.ServiceLoader;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Static utility class for service providers.
+ */
+public class Providers {
+
+    /**
+     * Load from {@link ServiceLoader}, sorting first objects whose classes are
+     * annotated with {@link DefaultProvider}.
+     * 
+     * @param <T>
+     *            provider type
+     * @param type
+     *            provider type instance
+     * @return {@link Iterable} of {@code T}
+     */
+    public static <T> Iterable<T> load(Class<T> type) {
+        return stream(type)::iterator;
+    }
+
+    /**
+     * Load from {@link ServiceLoader}, sorting first objects whose classes are
+     * annotated with {@link DefaultProvider}.
+     *
+     * @param <T>
+     *            provider type
+     * @param type
+     *            provider type instance
+     * @return {@link Stream} of {@code T}
+     */
+    public static <T> Stream<T> stream(Class<T> type) {
+        return StreamSupport.stream(ServiceLoader.load(type, type.getClassLoader()).spliterator(), false)
+            .sorted(compareProviders());
+    }
+
+    private static final <T> Comparator<T> compareProviders() {
+        return Comparator.<T, Boolean> comparing(o -> o.getClass().getAnnotation(DefaultProvider.class) == null)
+            .thenComparingInt(Object::hashCode);
+    }
+
+    private Providers() {}
+}
diff --git a/src/main/org/apache/ant/s3/build/spi/StringConversionsProvider.java b/src/main/org/apache/ant/s3/build/spi/StringConversionsProvider.java
new file mode 100644
index 0000000..5b17022
--- /dev/null
+++ b/src/main/org/apache/ant/s3/build/spi/StringConversionsProvider.java
@@ -0,0 +1,94 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build.spi;
+
+import static software.amazon.awssdk.utils.FunctionalUtils.safeSupplier;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.apache.commons.lang3.reflect.TypeUtils;
+import org.apache.commons.lang3.tuple.Pair;
+
+import software.amazon.awssdk.utils.FunctionalUtils.UnsafeSupplier;
+
+/**
+ * {@code abstract} SPI class for {@link String} conversions.
+ */
+public abstract class StringConversionsProvider
+    extends IntrospectingProviderBase<Map.Entry<Class<?>, Function<String, ?>>>
+    implements Supplier<Map<Class<?>, Function<String, ?>>> {
+
+    private static final TypeVariable<?> FUNCTION_RESULT = Function.class.getTypeParameters()[1];
+
+    /**
+     * Create a new {@link StringConversionsProvider}.
+     */
+    protected StringConversionsProvider() {
+        final Type t = TypeUtils.parameterize(Function.class, String.class, TypeUtils.wildcardType().build());
+
+        fieldFilter(filter -> filter.and(type(t)));
+
+        methodFilter(filter -> filter.and(args()).and(returns(t)));
+    }
+
+    /**
+     * Default implementation reflectively locates all {@code public} no-arg
+     * methods and {@code public final} fields returning {@link Function} of
+     * {@link String} to some other type.
+     * 
+     * @return {@link Map}
+     */
+    @Override
+    public final Map<Class<?>, Function<String, ?>> get() {
+        final Map<Class<?>, Function<String, ?>> result = new LinkedHashMap<>();
+
+        introspect(e -> result.put(e.getKey(), e.getValue()));
+
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Optional<Map.Entry<Class<?>, Function<String, ?>>> map(Field f) {
+        return map(f.getGenericType(), () -> f.get(this));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Optional<Map.Entry<Class<?>, Function<String, ?>>> map(Method m) {
+        return map(m.getGenericReturnType(), () -> m.invoke(this));
+    }
+
+    @SuppressWarnings("unchecked")
+    private Optional<Map.Entry<Class<?>, Function<String, ?>>> map(Type t, UnsafeSupplier<?> fnSupplier) {
+        return Optional
+            .of(Pair.of(TypeUtils.getRawType(TypeUtils.getTypeArguments(t, Function.class).get(FUNCTION_RESULT), null),
+                (Function<String, ?>) safeSupplier(fnSupplier).get()));
+    }
+}
diff --git a/src/main/org/apache/ant/s3/credentials/CredentialsConfiguringSuppliersProvider.java b/src/main/org/apache/ant/s3/credentials/CredentialsConfiguringSuppliersProvider.java
new file mode 100644
index 0000000..701edc8
--- /dev/null
+++ b/src/main/org/apache/ant/s3/credentials/CredentialsConfiguringSuppliersProvider.java
@@ -0,0 +1,513 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.credentials;
+
+import java.util.Locale;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.apache.ant.s3.Exceptions;
+import org.apache.ant.s3.ProjectUtils;
+import org.apache.ant.s3.build.Builder;
+import org.apache.ant.s3.build.ClassFinder;
+import org.apache.ant.s3.build.ConfiguringSupplier;
+import org.apache.ant.s3.build.MetaBuilderByType;
+import org.apache.ant.s3.build.spi.ConfiguringSuppliersProvider;
+import org.apache.ant.s3.strings.ClassNames;
+import org.apache.ant.s3.strings.ClassNames.Direction;
+import org.apache.ant.s3.strings.PackageNames;
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.ProjectComponent;
+import org.kohsuke.MetaInfServices;
+
+import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.ProcessCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.ProfileCredentialsProviderFactory;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.internal.LazyAwsCredentialsProvider;
+import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider;
+import software.amazon.awssdk.services.sts.auth.StsAssumeRoleWithSamlCredentialsProvider;
+import software.amazon.awssdk.services.sts.auth.StsAssumeRoleWithWebIdentityCredentialsProvider;
+import software.amazon.awssdk.services.sts.auth.StsGetFederationTokenCredentialsProvider;
+import software.amazon.awssdk.services.sts.auth.StsGetSessionTokenCredentialsProvider;
+import software.amazon.awssdk.utils.builder.Buildable;
+
+/**
+ * {@link ConfiguringSuppliersProvider} for AWS credentials.
+ */
+@MetaInfServices
+public class CredentialsConfiguringSuppliersProvider extends ConfiguringSuppliersProvider {
+    /**
+     * Base {@link ConfiguringSupplier} implementation for internal stuff.
+     *
+     * @param <T>
+     *            supplied type
+     */
+    public static abstract class BaseConfiguringSupplier<T> extends ProjectComponent
+        implements ConfiguringSupplier<T>, ProjectUtils {
+        /**
+         * Create a new {@link BaseConfiguringSupplier}.
+         * 
+         * @param project
+         *            Ant {@link Project}
+         */
+        protected BaseConfiguringSupplier(Project project) {
+            setProject(project);
+        }
+    }
+
+    /**
+     * {@link StaticCredentialsProvider} {@link ConfiguringSupplier}. Supports
+     * {@link AwsBasicCredentials} only.
+     */
+    public static class StaticCredentialsProviderConfiguringSupplier
+        extends BaseConfiguringSupplier<StaticCredentialsProvider> {
+        private String accessKey;
+        private String secretKey;
+
+        /**
+         * Create a new {@link StaticCredentialsProviderConfiguringSupplier}.
+         * 
+         * @param project
+         *            Ant {@link Project}
+         */
+        private StaticCredentialsProviderConfiguringSupplier(Project project) {
+            super(project);
+        }
+
+        /**
+         * Set the accessKey.
+         * 
+         * @param accessKey
+         *            {@link String}
+         */
+        public void setAccessKey(String accessKey) {
+            this.accessKey = accessKey;
+        }
+
+        /**
+         * Set the secretKey.
+         * 
+         * @param secretKey
+         *            {@link String}
+         */
+        public void setSecretKey(String secretKey) {
+            this.secretKey = secretKey;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void setDynamicAttribute(String uri, String localName, String qName, String value)
+            throws BuildException {
+            switch (StringUtils.lowerCase(localName, Locale.US)) {
+            case "accesskey":
+                setAccessKey(value);
+                break;
+            case "secretkey":
+                setSecretKey(value);
+                break;
+            default:
+                super.setDynamicAttribute(uri, localName, qName, value);
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public StaticCredentialsProvider get() {
+            return StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey));
+        }
+    }
+
+    /**
+     * {@link ConfiguringSupplier} implementation for concrete {@code *Builder}
+     * types that do not implement {@link Buildable}.
+     *
+     * @param <B>
+     *            {@code *Builder}
+     * @param <T>
+     *            built/supplied type
+     */
+    public static class NonBuildableBuilderBuildConfiguringSupplier<B, T> extends BaseConfiguringSupplier<T> {
+
+        private final B sdkBuilder;
+        private final Builder<B> builder;
+        private final Function<B, T> get;
+
+        @SuppressWarnings({ "unchecked", "rawtypes" })
+        private NonBuildableBuilderBuildConfiguringSupplier(Project project, B builder, Function<B, T> get) {
+            super(project);
+            this.sdkBuilder = builder;
+            this.builder = new Builder(builder.getClass(), project);
+            this.get = get;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void setDynamicAttribute(String uri, String localName, String qName, String value)
+            throws BuildException {
+            builder.setDynamicAttribute(uri, localName, qName, value);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public Object createDynamicElement(String uri, String localName, String qName) throws BuildException {
+            return builder.createDynamicElement(uri, localName, qName);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public T get() {
+            builder.accept(sdkBuilder);
+            return get.apply(sdkBuilder);
+        }
+    }
+
+    /**
+     * {@link ConfiguringSupplier} for types that build with no configuration.
+     *
+     * @param <T>
+     *            supplied type
+     */
+    public static class NoConfigConfiguringSupplier<T> extends BaseConfiguringSupplier<T> {
+        private final Supplier<T> get;
+
+        private NoConfigConfiguringSupplier(Project project, Supplier<T> get) {
+            super(project);
+            this.get = get;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public T get() {
+            return get.get();
+        }
+    }
+
+    /**
+     * {@link ConfiguringSupplier} for {@link LazyAwsCredentialsProvider}.
+     * Requires a single nested {@code provider} element which is
+     * {@link CredentialsConfiguringSuppliersProvider#credentialsProviderConfiguringSupplier(Project)}.
+     */
+    public class LazyCredentialsProviderConfiguringSupplier
+        extends BaseConfiguringSupplier<LazyAwsCredentialsProvider> {
+        private ConfiguringSupplier<AwsCredentialsProvider> provider;
+
+        private LazyCredentialsProviderConfiguringSupplier(Project project) {
+            super(project);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public Object createDynamicElement(String uri, String localName, String qName) throws BuildException {
+            if ("provider".equals(localName)) {
+                Exceptions.raiseUnless(provider == null, buildExceptionTriggered(), new IllegalStateException(),
+                    "provider already created");
+                provider = credentialsProviderConfiguringSupplier(getProject());
+                return provider;
+            }
+            return super.createDynamicElement(uri, localName, qName);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public LazyAwsCredentialsProvider get() {
+            Exceptions.raiseIf(provider == null, buildExceptionTriggered(), new IllegalStateException(),
+                "provider not configured");
+            return LazyAwsCredentialsProvider.create(provider);
+        }
+    }
+
+    /**
+     * Classname of only known implementation of
+     * {@link ProfileCredentialsProviderFactory}.
+     */
+    public static final String SSO_CPF_CLASSNAME =
+        "software.amazon.awssdk.services.sso.auth.SsoProfileCredentialsProviderFactory";
+
+    /**
+     * Package name of AWS services package.
+     */
+    public static final String SERVICES_PACKAGE = "software.amazon.awssdk.services";
+
+    /**
+     * Produce a {@link ConfiguringSupplier} of {@link AwsCredentialsProvider}.
+     * This is a {@link MetaBuilderByType} configured to search for
+     * {@link AwsCredentialsProvider} types given an {@code @impl} "fragment"
+     * that is:
+     * <ul>
+     * <li>Tested as a FQ classname</li>
+     * <li>Plugged into a matrix of: packages
+     * <ul>
+     * <li>0-2 ancestors of {@link AwsCredentialsProvider}</li>
+     * <li>this package</li>
+     * </ul>
+     * X classname suffixes as segments of {@link AwsCredentialsProvider},
+     * successively trimmed from the LHS.</li>
+     * <li>If unspecified, defaulted to {@link StaticCredentialsProvider}</li>
+     * </ul>
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link MetaBuilderByType} of {@link AwsCredentialsProvider}
+     */
+    public MetaBuilderByType<AwsCredentialsProvider> credentialsProviderConfiguringSupplier(Project project) {
+        return new MetaBuilderByType<>(project, AwsCredentialsProvider.class,
+            new ClassFinder(
+                PackageNames.of(AwsCredentialsProvider.class).ancestors(0, 2).andThen(PackageNames.of(getClass())),
+                ClassNames.of(AwsCredentialsProvider.class).segments(Direction.FROM_LEFT)),
+            StaticCredentialsProvider.class);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} of
+     * {@link ProfileCredentialsProviderFactory}. This is a
+     * {@link MetaBuilderByType} configured to search for
+     * {@link ProfileCredentialsProviderFactory} types given an {@code @impl}
+     * "fragment" that is:
+     * <ul>
+     * <li>Tested as a FQ classname</li>
+     * <li>Plugged into a matrix of: packages
+     * <ul>
+     * <li>Package of {@link ProfileCredentialsProviderFactory}</li>
+     * <li>Package of {@link #SSO_CPF_CLASSNAME} (if class present on classpath;
+     * prioritizes {@code sso} as explicit impl key)</li>
+     * <li>{@link #SERVICES_PACKAGE} ({@value #SERVICES_PACKAGE})</li>
+     * <li>1-2 ancestors of {@link ProfileCredentialsProviderFactory}</li>
+     * </ul>
+     * X classname suffixes as segments of
+     * {@link ProfileCredentialsProviderFactory}, successively trimmed from the
+     * LHS.</li>
+     * <li>If unspecified, defaulted to {@link #SSO_CPF_CLASSNAME} if present on
+     * classpath</li>
+     * </ul>
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link MetaBuilderByType} of
+     *         {@link ProfileCredentialsProviderFactory}
+     */
+    public MetaBuilderByType<ProfileCredentialsProviderFactory> profileCredentialsProviderFactoryConfiguringSupplier(
+        Project project) {
+        final PackageNames pcpf = PackageNames.of(ProfileCredentialsProviderFactory.class);
+        PackageNames packageNames = pcpf;
+
+        Class<? extends ProfileCredentialsProviderFactory> defaultImpl;
+        try {
+            defaultImpl = ClassUtils.getClass(SSO_CPF_CLASSNAME).asSubclass(ProfileCredentialsProviderFactory.class);
+            packageNames = PackageNames.of(defaultImpl).andThen(packageNames);
+        } catch (ClassNotFoundException e) {
+            defaultImpl = null;
+        }
+        packageNames = packageNames.andThen(PackageNames.of(SERVICES_PACKAGE)).andThen(pcpf.ancestors(1, 2));
+
+        return new MetaBuilderByType<>(project, ProfileCredentialsProviderFactory.class, new ClassFinder(packageNames,
+            ClassNames.of(ProfileCredentialsProviderFactory.class).segments(Direction.FROM_LEFT)), defaultImpl);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link StaticCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link StaticCredentialsProviderConfiguringSupplier}
+     */
+    public StaticCredentialsProviderConfiguringSupplier staticCredentialsProviderConfiguringSupplier(Project project) {
+        return new StaticCredentialsProviderConfiguringSupplier(project);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link DefaultCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link NonBuildableBuilderBuildConfiguringSupplier}
+     */
+    public ConfiguringSupplier<DefaultCredentialsProvider> defaultCredentialsProviderConfiguringSupplier(
+        Project project) {
+        return new NonBuildableBuilderBuildConfiguringSupplier<>(project, DefaultCredentialsProvider.builder(),
+            DefaultCredentialsProvider.Builder::build);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link AwsCredentialsProviderChain}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link NonBuildableBuilderBuildConfiguringSupplier}
+     */
+    public ConfiguringSupplier<AwsCredentialsProviderChain> chain(Project project) {
+        return new NonBuildableBuilderBuildConfiguringSupplier<>(project, AwsCredentialsProviderChain.builder(),
+            AwsCredentialsProviderChain.Builder::build);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link ProcessCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link NonBuildableBuilderBuildConfiguringSupplier}
+     */
+    public ConfiguringSupplier<ProcessCredentialsProvider> process(Project project) {
+        return new NonBuildableBuilderBuildConfiguringSupplier<>(project, ProcessCredentialsProvider.builder(),
+            ProcessCredentialsProvider.Builder::build);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link StsAssumeRoleCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link NonBuildableBuilderBuildConfiguringSupplier}
+     */
+    public ConfiguringSupplier<StsAssumeRoleCredentialsProvider> stsAssumeRole(Project project) {
+        return new NonBuildableBuilderBuildConfiguringSupplier<>(project, StsAssumeRoleCredentialsProvider.builder(),
+            StsAssumeRoleCredentialsProvider.Builder::build);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link StsAssumeRoleWithSamlCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link NonBuildableBuilderBuildConfiguringSupplier}
+     */
+    public ConfiguringSupplier<StsAssumeRoleWithSamlCredentialsProvider> stsAssumeRoleSaml(Project project) {
+        return new NonBuildableBuilderBuildConfiguringSupplier<>(project,
+            StsAssumeRoleWithSamlCredentialsProvider.builder(),
+            StsAssumeRoleWithSamlCredentialsProvider.Builder::build);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link StsAssumeRoleWithWebIdentityCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link NonBuildableBuilderBuildConfiguringSupplier}
+     */
+    public ConfiguringSupplier<StsAssumeRoleWithWebIdentityCredentialsProvider> stsAssumeRoleWebId(Project project) {
+        return new NonBuildableBuilderBuildConfiguringSupplier<>(project,
+            StsAssumeRoleWithWebIdentityCredentialsProvider.builder(),
+            StsAssumeRoleWithWebIdentityCredentialsProvider.Builder::build);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link StsGetFederationTokenCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link NonBuildableBuilderBuildConfiguringSupplier}
+     */
+    public ConfiguringSupplier<StsGetFederationTokenCredentialsProvider> stsFederationToken(Project project) {
+        return new NonBuildableBuilderBuildConfiguringSupplier<>(project,
+            StsGetFederationTokenCredentialsProvider.builder(),
+            StsGetFederationTokenCredentialsProvider.Builder::build);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link StsGetSessionTokenCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link NonBuildableBuilderBuildConfiguringSupplier}
+     */
+    public ConfiguringSupplier<StsGetSessionTokenCredentialsProvider> stsSessionToken(Project project) {
+        return new NonBuildableBuilderBuildConfiguringSupplier<>(project,
+            StsGetSessionTokenCredentialsProvider.builder(), StsGetSessionTokenCredentialsProvider.Builder::build);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link AnonymousCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link NoConfigConfiguringSupplier}
+     */
+    public ConfiguringSupplier<AnonymousCredentialsProvider> anonymous(Project project) {
+        return new NoConfigConfiguringSupplier<>(project, AnonymousCredentialsProvider::create);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link EnvironmentVariableCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link NoConfigConfiguringSupplier}
+     */
+    public ConfiguringSupplier<EnvironmentVariableCredentialsProvider> environmentVariable(Project project) {
+        return new NoConfigConfiguringSupplier<>(project, EnvironmentVariableCredentialsProvider::create);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link SystemPropertyCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link NoConfigConfiguringSupplier}
+     */
+    public ConfiguringSupplier<SystemPropertyCredentialsProvider> systemProperty(Project project) {
+        return new NoConfigConfiguringSupplier<>(project, SystemPropertyCredentialsProvider::create);
+    }
+
+    /**
+     * Produce a {@link ConfiguringSupplier} for
+     * {@link LazyAwsCredentialsProvider}.
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link LazyCredentialsProviderConfiguringSupplier}
+     */
+    public ConfiguringSupplier<LazyAwsCredentialsProvider> lazy(Project project) {
+        return new LazyCredentialsProviderConfiguringSupplier(project);
+    }
+}
diff --git a/src/main/org/apache/ant/s3/http/ClientConfiguringSuppliersProvider.java b/src/main/org/apache/ant/s3/http/ClientConfiguringSuppliersProvider.java
new file mode 100644
index 0000000..b2ddba9
--- /dev/null
+++ b/src/main/org/apache/ant/s3/http/ClientConfiguringSuppliersProvider.java
@@ -0,0 +1,82 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.http;
+
+import java.util.Map;
+
+import org.apache.ant.s3.InlineProperties;
+import org.apache.ant.s3.ProjectUtils;
+import org.apache.ant.s3.build.ConfiguringSupplier;
+import org.apache.ant.s3.build.StringConversions;
+import org.apache.ant.s3.build.spi.ConfiguringSuppliersProvider;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.ProjectComponent;
+import org.kohsuke.MetaInfServices;
+
+import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.awssdk.http.SdkHttpConfigurationOption;
+
+/**
+ * {@link ConfiguringSuppliersProvider} for {@link SdkHttpClient}.
+ */
+@MetaInfServices
+public class ClientConfiguringSuppliersProvider extends ConfiguringSuppliersProvider {
+
+    /**
+     * {@link SdkHttpClient} {@link ConfiguringSupplier}.
+     */
+    public static class HttpClientSupplier extends ProjectComponent
+        implements ConfiguringSupplier<SdkHttpClient>, ProjectUtils {
+        private final InlineProperties attributes;
+
+        private HttpClientSupplier(Project project) {
+            setProject(project);
+            attributes = new InlineProperties(project);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public Object createDynamicElement(String uri, String localName, String qName) throws BuildException {
+            return attributes.createDynamicElement(uri, localName, qName);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @SuppressWarnings({ "unchecked", "rawtypes" })
+        @Override
+        public SdkHttpClient get() {
+            return new DefaultSdkHttpClientBuilder().buildWithDefaults(
+                StringConversions.attributes(SdkHttpConfigurationOption.class, (Map) attributes.getProperties()));
+        }
+    }
+
+    /**
+     * Get an {@link HttpClientSupplier}
+     * 
+     * @param project
+     *            Ant {@link Project}
+     * @return {@link HttpClientSupplier}
+     */
+    public HttpClientSupplier httpClientSupplier(Project project) {
+        return new HttpClientSupplier(project);
+    }
+}
diff --git a/src/main/org/apache/ant/s3/strings/ClassNames.java b/src/main/org/apache/ant/s3/strings/ClassNames.java
new file mode 100644
index 0000000..cd3eb19
--- /dev/null
+++ b/src/main/org/apache/ant/s3/strings/ClassNames.java
@@ -0,0 +1,234 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.strings;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.apache.ant.s3.Exceptions;
+import org.apache.commons.lang3.StringUtils;
+
+import software.amazon.awssdk.utils.Lazy;
+
+/**
+ * (Simple) class names.
+ */
+@FunctionalInterface
+public interface ClassNames extends Strings {
+
+    /**
+     * Segment direction.
+     */
+    public enum Direction {
+        FROM_LEFT {
+            @Override
+            List<String> window(List<String> source, int removeSegments) {
+                return source.subList(removeSegments, source.size());
+            }
+        },
+        FROM_RIGHT {
+            @Override
+            List<String> window(List<String> source, int removeSegments) {
+                return source.subList(0, source.size() - removeSegments);
+            }
+        };
+
+        /**
+         * Get a "window" (sublist) into the specified {@code source}
+         * {@link List} removing the specified number of segments (elements)
+         * from {@code this} direction.
+         * 
+         * @param source
+         * @param removeSegments
+         * @return {@link List} of {@link String}
+         */
+        abstract List<String> window(List<String> source, int removeSegments);
+    }
+
+    /**
+     * Get an empty {@link ClassNames}.
+     * 
+     * @return {@link ClassNames}
+     */
+    public static ClassNames empty() {
+        return Collections::emptyIterator;
+    }
+
+    /**
+     * Get a {@link ClassNames} representing the simple names of the specified
+     * classes.
+     * 
+     * @param clazz
+     *            first
+     * @param clazzes
+     *            additional
+     * @return {@link ClassNames}
+     */
+    public static ClassNames of(Class<?> clazz, Class<?>... clazzes) {
+        return of(Stream.concat(Stream.of(clazz), Stream.of(clazzes)).map(Class::getSimpleName)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+    }
+
+    /**
+     * Get {@link ClassNames} of the specified values (not checked).
+     * 
+     * @param value
+     *            first
+     * @param values
+     *            additional
+     * @return {@link ClassNames}
+     */
+    public static ClassNames of(String value, String... values) {
+        return of(Strings.of(value, values));
+    }
+
+    /**
+     * Get {@link ClassNames} of the specified values.
+     *
+     * @param names
+     *            should be repeatable {@link Iterable}
+     * @return {@link ClassNames}
+     */
+    public static ClassNames of(Iterable<String> names) {
+        return names instanceof ClassNames ? (ClassNames) names : names::iterator;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @return {@link ClassNames}
+     */
+    default ClassNames andThen(Iterable<String> next) {
+        return of(Strings.super.andThen(next));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @return {@link ClassNames}
+     */
+    default ClassNames sorted() {
+        return of(Strings.super.sorted());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @return {@link ClassNames}
+     */
+    default ClassNames sorted(Comparator<? super String> cmp) {
+        return of(Strings.super.sorted(cmp));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @return {@link ClassNames}
+     */
+    @Override
+    default ClassNames reverse() {
+        return of(Strings.super.reverse());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @return {@link ClassNames}
+     */
+    default ClassNames distinct() {
+        return of(Strings.super.distinct());
+    }
+
+    /**
+     * Get a {@link ClassNames} trimming {@code trimmed} segments from elements
+     * of {@code this} in the specified {@code direction}.
+     * 
+     * @param direction
+     *            from which to trim segments
+     * @param trimmed
+     *            number of segments to remove
+     * @return {@link ClassNames}
+     */
+    default ClassNames segment(Direction direction, int trimmed) {
+        return segments(direction, trimmed, trimmed);
+    }
+
+    /**
+     * Get a {@link ClassNames} trimming segments from elements of {@code this}
+     * in the specified {@code direction}.
+     * 
+     * @param direction
+     *            from which to trim segments
+     * @return {@link ClassNames}
+     */
+    default ClassNames segments(Direction direction) {
+        final Function<? super String, ? extends Stream<? extends String>> expand = s -> {
+            final List<String> segments = Arrays.asList(StringUtils.splitByCharacterTypeCamelCase(s));
+            if (segments.isEmpty()) {
+                return Stream.empty();
+            }
+            return IntStream.range(0, segments.size()).mapToObj(n -> direction.window(segments, n))
+                .map(w -> StringUtils.join(w, null));
+        };
+        @SuppressWarnings("resource")
+        final Lazy<Iterable<String>> lazy = new Lazy<>(() -> stream().flatMap(expand).collect(Collectors.toList()));
+
+        // use lambda to defer evaluation
+        return of(() -> lazy.getValue().iterator());
+    }
+
+    /**
+     * Get a {@link ClassNames} exposing variants removing successively more
+     * camel-case segments of the base content.
+     * 
+     * @param direction
+     *            from which to trim segments
+     * @param min
+     *            number of segments to remove
+     * @param max
+     *            number of segments to remove
+     * @return {@link ClassNames}
+     */
+    default ClassNames segments(Direction direction, int min, int max) {
+        Exceptions.raiseIf(direction == null || min < 0 || max < 0, IllegalArgumentException::new,
+            "Invalid arguments(%s, %d, %d)", direction, min, max);
+
+        if (min == 0 && max == 0) {
+            return this;
+        }
+        final Function<? super String, ? extends Stream<? extends String>> expand = s -> {
+            final List<String> segments = Arrays.asList(StringUtils.splitByCharacterTypeCamelCase(s));
+            if (min >= segments.size()) {
+                return Stream.empty();
+            }
+            return IntStream.rangeClosed(min, Math.min(max, segments.size() - 1))
+                .mapToObj(n -> direction.window(segments, n)).map(w -> StringUtils.join(w, null));
+        };
+        @SuppressWarnings("resource")
+        final Lazy<Iterable<String>> lazy = new Lazy<>(() -> stream().flatMap(expand).collect(Collectors.toList()));
+
+        // use lambda to defer evaluation
+        return of(() -> lazy.getValue().iterator());
+    }
+}
diff --git a/src/main/org/apache/ant/s3/strings/PackageNames.java b/src/main/org/apache/ant/s3/strings/PackageNames.java
new file mode 100644
index 0000000..7813b09
--- /dev/null
+++ b/src/main/org/apache/ant/s3/strings/PackageNames.java
@@ -0,0 +1,214 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.strings;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.ant.s3.Exceptions;
+import org.apache.commons.lang3.Range;
+
+import software.amazon.awssdk.utils.Lazy;
+
+/**
+ * Package names.
+ */
+@FunctionalInterface
+public interface PackageNames extends Strings {
+
+    /**
+     * Get an empty {@link PackageNames}.
+     * 
+     * @return {@link PackageNames}
+     */
+    public static PackageNames empty() {
+        return Collections::emptyIterator;
+    }
+
+    /**
+     * Get {@link PackageNames} of the specified root classes.
+     * 
+     * @param rootClass
+     *            first
+     * @param rootClasses
+     *            additional
+     * @return {@link PackageNames}
+     */
+    public static PackageNames of(Class<?> rootClass, Class<?>... rootClasses) {
+        return of(Stream.concat(Stream.of(rootClass), Stream.of(rootClasses)).map(c -> c.getPackage().getName())
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+    }
+
+    /**
+     * Get {@link PackageNames} of the specified values (not checked).
+     * 
+     * @param name
+     *            first
+     * @param names
+     *            additional
+     * @return {@link PackageNames}
+     */
+    public static PackageNames of(String name, String... names) {
+        return of(Strings.of(name, names));
+    }
+
+    /**
+     * Get {@link PackageNames} of the specified values (not checked).
+     * 
+     * @param names
+     *            should be repeatable {@link Iterable}
+     * @return {@link PackageNames}
+     */
+    public static PackageNames of(Iterable<String> names) {
+        return names instanceof PackageNames ? (PackageNames) names : names::iterator;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @return {@link PackageNames}
+     */
+    @Override
+    default PackageNames andThen(Iterable<String> next) {
+        return of(Strings.super.andThen(next));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @return {@link PackageNames}
+     */
+    @Override
+    default PackageNames sorted() {
+        return of(Strings.super.sorted());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @return {@link PackageNames}
+     */
+    @Override
+    default PackageNames sorted(Comparator<? super String> cmp) {
+        return of(Strings.super.sorted(cmp));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @return {@link PackageNames}
+     */
+    @Override
+    default PackageNames reverse() {
+        return of(Strings.super.reverse());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @return {@link PackageNames}
+     */
+    default PackageNames distinct() {
+        return of(Strings.super.distinct());
+    }
+
+    /**
+     * Get a {@link PackageNames} exposing the {@code Nth} ancestor package of
+     * {@code this}, where {@code N} is {@code displacement}. Syntactic sugar
+     * for {@link #ancestors(int, int)} with {@code displacement} as both
+     * {@code min} and {@code max}.
+     * 
+     * @param displacement
+     *            distance
+     * @return {@link PackageNames}
+     */
+    default PackageNames ancestor(int displacement) {
+        return ancestors(displacement, displacement);
+    }
+
+    /**
+     * Get a {@link PackageNames} exposing all ancestor packages of
+     * {@code this}.
+     * 
+     * @return {@link PackageNames}
+     */
+    default PackageNames ancestors() {
+        final PackageNames wrapped = this;
+
+        @SuppressWarnings("resource")
+        final Lazy<Iterable<String>> lazy = new Lazy<>(() -> wrapped.stream().flatMap(s -> {
+            final StringBuilder b = new StringBuilder(s);
+            final Stream.Builder<String> result = Stream.builder();
+
+            while (true) {
+                result.accept(b.toString());
+                final int lastDot = b.lastIndexOf(".");
+                if (lastDot < 0) {
+                    break;
+                }
+                b.setLength(lastDot);
+            }
+            return result.build();
+        }).collect(Collectors.toList()));
+
+        // use lambda to defer evaluation
+        return of(() -> lazy.getValue().iterator());
+    }
+
+    /**
+     * Get a {@link PackageNames} exposing ancestor packages of {@code this}.
+     * 
+     * @param min
+     *            levels
+     * @param max
+     *            levels
+     * @return {@link PackageNames}
+     */
+    default PackageNames ancestors(int min, int max) {
+        Exceptions.raiseIf(min < 0 || max < 0, IllegalArgumentException::new, "Invalid arguments(%d, %d)", min, max);
+
+        if (min == 0 && max == 0) {
+            return this;
+        }
+        final PackageNames wrapped = this;
+        final Range<Integer> generations = Range.between(min, max);
+
+        @SuppressWarnings("resource")
+        final Lazy<Iterable<String>> lazy = new Lazy<>(() -> wrapped.stream().flatMap(s -> {
+            final StringBuilder b = new StringBuilder(s);
+            final Stream.Builder<String> result = Stream.builder();
+
+            for (int n = 0; !generations.isBefore(n); n++) {
+                if (generations.contains(n)) {
+                    result.accept(b.toString());
+                }
+                final int lastDot = b.lastIndexOf(".");
+                if (lastDot < 0) {
+                    break;
+                }
+                b.setLength(lastDot);
+            }
+            return result.build();
+        }).collect(Collectors.toList()));
+
+        // use lambda to defer evaluation
+        return of(() -> lazy.getValue().iterator());
+    }
+}
diff --git a/src/main/org/apache/ant/s3/strings/Strings.java b/src/main/org/apache/ant/s3/strings/Strings.java
new file mode 100644
index 0000000..a747211
--- /dev/null
+++ b/src/main/org/apache/ant/s3/strings/Strings.java
@@ -0,0 +1,128 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.strings;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Fun with {@link String}s.
+ */
+@FunctionalInterface
+public interface Strings extends Iterable<String> {
+
+    /**
+     * Get an empty {@link Strings}.
+     * 
+     * @return {@link Strings}
+     */
+    public static Strings empty() {
+        return Collections::emptyIterator;
+    }
+
+    /**
+     * Get {@link Strings} of the specified values.
+     * 
+     * @param value
+     *            first
+     * @param values
+     *            additional
+     * @return {@link Strings}
+     */
+    public static Strings of(String value, String... values) {
+        return of(
+            Stream.concat(Stream.of(value), Stream.of(values)).collect(Collectors.toCollection(LinkedHashSet::new)));
+    }
+
+    /**
+     * Get {@link Strings} of the specified values.
+     *
+     * @param names
+     *            should be repeatable {@link Iterable}
+     * @return {@link Strings}
+     */
+    public static Strings of(Iterable<String> names) {
+        return names instanceof Strings ? (Strings) names : names::iterator;
+    }
+
+    /**
+     * Get a {@link Strings} combining {@code this} with {@code next}.
+     * 
+     * @param next
+     *            subsequent {@link String}s
+     * @return {@link Strings}
+     */
+    default Strings andThen(Iterable<String> next) {
+        return () -> Stream.of(this, of(next)).flatMap(Strings::stream).iterator();
+    }
+
+    /**
+     * Get a {@link Strings} sorting {@code this} by natural order.
+     * 
+     * @return {@link Strings}
+     */
+    default Strings sorted() {
+        return sorted(Comparator.naturalOrder());
+    }
+
+    /**
+     * Get a {@link Strings} sorting {@code this} by the specified
+     * {@link Comparator}.
+     * 
+     * @param cmp
+     *            {@link Comparator} to sort by
+     * @return {@link Strings}
+     */
+    default Strings sorted(Comparator<? super String> cmp) {
+        return () -> stream().sorted(cmp).iterator();
+    }
+
+    /**
+     * Get a {@link Strings} reversing {@code this}.
+     * 
+     * @return {@link Strings}
+     */
+    default Strings reverse() {
+        final Deque<String> contents = new LinkedList<>();
+        this.forEach(contents::push);
+        return of(contents);
+    }
+
+    /**
+     * Get distinct {@link Strings} from {@code this}.
+     * 
+     * @return {@link Strings}
+     */
+    default Strings distinct() {
+        return () -> stream().distinct().iterator();
+    }
+
+    /**
+     * Get a {@link Stream} of our contents.
+     * 
+     * @return {@link Stream} of {@link String}
+     */
+    default Stream<String> stream() {
+        return StreamSupport.stream(spliterator(), false);
+    }
+}
diff --git a/src/tests/antunit/s3-test-base.xml b/src/tests/antunit/s3-test-base.xml
index 399c197..0d23b7d 100644
--- a/src/tests/antunit/s3-test-base.xml
+++ b/src/tests/antunit/s3-test-base.xml
@@ -49,12 +49,11 @@
     project.setProperty('s3.endpoint', "https://localhost:${DEFAULT_HTTPS_PORT}")
   </groovy>
 
-  <s3:client id="s3">
-    <credentials accesskey="foo" secretkey="bar" />
-    <http>
+  <s3:client id="s3" endpointoverride="${s3.endpoint}" region="us-east-1">
+    <credentialsprovider accesskey="foo" secretkey="bar" />
+    <httpclient>
       <TRUST_ALL_CERTIFICATES>true</TRUST_ALL_CERTIFICATES>
-    </http>
-    <builder endpointOverride="${s3.endpoint}" region="us-east-1" />
+    </httpclient>
   </s3:client>
 
   <groovy>
diff --git a/src/tests/junit/org/apache/ant/s3/build/StringConversionsTest.java b/src/tests/junit/org/apache/ant/s3/build/StringConversionsTest.java
new file mode 100644
index 0000000..ca48e19
--- /dev/null
+++ b/src/tests/junit/org/apache/ant/s3/build/StringConversionsTest.java
@@ -0,0 +1,156 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.build;
+
+import static java.lang.reflect.Modifier.FINAL;
+import static java.lang.reflect.Modifier.PUBLIC;
+import static java.lang.reflect.Modifier.STATIC;
+import static org.assertj.core.api.Assertions.assertThat;
+import static software.amazon.awssdk.utils.FunctionalUtils.safeFunction;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URI;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.apache.commons.lang3.reflect.TypeLiteral;
+import org.junit.Test;
+
+import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode;
+import software.amazon.awssdk.regions.Region;
+
+public class StringConversionsTest {
+    enum MetaSyntacticVariable {
+        FOO, BAR, BAZ;
+    }
+
+    @Test
+    public void testWrapperConversions() {
+        assertThat(StringConversions.<Byte> as(Byte.class, String.valueOf(Byte.MAX_VALUE)))
+            .isEqualTo(Byte.valueOf(Byte.MAX_VALUE));
+
+        assertThat(StringConversions.<Short> as(Short.class, String.valueOf(Short.MAX_VALUE)))
+            .isEqualTo(Short.MAX_VALUE);
+
+        assertThat(StringConversions.<Character> as(Character.class, String.valueOf(Character.MAX_VALUE)))
+            .isEqualTo(Character.MAX_VALUE);
+
+        assertThat(StringConversions.<Integer> as(Integer.class, String.valueOf(Integer.MAX_VALUE)))
+            .isEqualTo(Integer.MAX_VALUE);
+
+        assertThat(StringConversions.<Long> as(Long.class, String.valueOf(Long.MAX_VALUE))).isEqualTo(Long.MAX_VALUE);
+        assertThat(StringConversions.<Float> as(Float.class, String.valueOf(Float.MAX_VALUE)))
+            .isEqualTo(Float.MAX_VALUE);
+        assertThat(StringConversions.<Double> as(Double.class, String.valueOf(Double.MAX_VALUE)))
+            .isEqualTo(Double.MAX_VALUE);
+        assertThat(StringConversions.<Boolean> as(Boolean.class, "true")).isTrue();
+    }
+
+    @Test
+    public void testPrimitiveConversions() {
+        assertThat(StringConversions.<Byte> as(Byte.TYPE, String.valueOf(Byte.MAX_VALUE)))
+            .isEqualTo(Byte.valueOf(Byte.MAX_VALUE));
+
+        assertThat(StringConversions.<Short> as(Short.TYPE, String.valueOf(Short.MAX_VALUE)))
+            .isEqualTo(Short.MAX_VALUE);
+
+        assertThat(StringConversions.<Character> as(Character.TYPE, String.valueOf(Character.MAX_VALUE)))
+            .isEqualTo(Character.MAX_VALUE);
+
+        assertThat(StringConversions.<Integer> as(Integer.TYPE, String.valueOf(Integer.MAX_VALUE)))
+            .isEqualTo(Integer.MAX_VALUE);
+
+        assertThat(StringConversions.<Long> as(Long.TYPE, String.valueOf(Long.MAX_VALUE))).isEqualTo(Long.MAX_VALUE);
+        assertThat(StringConversions.<Float> as(Float.TYPE, String.valueOf(Float.MAX_VALUE)))
+            .isEqualTo(Float.MAX_VALUE);
+        assertThat(StringConversions.<Double> as(Double.TYPE, String.valueOf(Double.MAX_VALUE)))
+            .isEqualTo(Double.MAX_VALUE);
+        assertThat(StringConversions.<Boolean> as(Boolean.TYPE, "true")).isTrue();
+    }
+
+    @Test
+    public void testOtherDefaultConversions() {
+        assertThat(StringConversions.<String> as(String.class, "foo")).isEqualTo("foo");
+        assertThat(StringConversions.<Duration> as(Duration.class, "PT66H")).isEqualTo(Duration.ofHours(66));
+        assertThat(StringConversions.<URI> as(URI.class, "https://ant.apache.org")).hasScheme("https")
+            .hasHost("ant.apache.org").hasNoPort().hasPath("").hasNoFragment().hasNoParameters();
+
+        assertThat(StringConversions.<Path> as(Path.class, System.getProperty("user.dir")))
+            .isEqualTo(new File(System.getProperty("user.dir")).toPath());
+
+        assertThat(StringConversions.<BigDecimal> as(BigDecimal.class, "999.999")).isEqualTo("999.999");
+
+        assertThat(StringConversions.<BigInteger> as(BigInteger.class, "999")).isEqualTo("999");
+        assertThat(StringConversions.<byte[]> as(byte[].class, "foo")).containsExactly('f', 'o', 'o');
+        assertThat(StringConversions.<char[]> as(char[].class, "foo")).containsExactly('f', 'o', 'o');
+    }
+
+    @Test
+    public void testEnumConversions() {
+        assertThat(StringConversions.<MetaSyntacticVariable> as(MetaSyntacticVariable.class, "foo"))
+            .isSameAs(MetaSyntacticVariable.FOO);
+        assertThat(StringConversions.<MetaSyntacticVariable> as(MetaSyntacticVariable.class, "BAR"))
+            .isSameAs(MetaSyntacticVariable.BAR);
+    }
+
+    @Test
+    public void testDefaultAwsConversions() {
+        for (DefaultsMode dm : DefaultsMode.values()) {
+            assertThat(StringConversions.<DefaultsMode> as(DefaultsMode.class, dm.toString())).isSameAs(dm);
+        }
+
+        final int psf = PUBLIC | STATIC | FINAL;
+
+        Stream.of(Region.class.getDeclaredFields()).filter(f -> (f.getModifiers() & psf) == psf)
+            .filter(f -> Region.class.equals(f.getType())).map(safeFunction(f -> f.get(null)))
+            .map(Region.class::cast)
+            .forEach(region -> assertThat(StringConversions.<Region> as(Region.class, region.id())).isSameAs(region));
+    }
+
+    @Test
+    public void testCommaDelimitedArrayConversion() {
+        assertThat(StringConversions.<MetaSyntacticVariable[]> as(MetaSyntacticVariable[].class, "foo,baz"))
+            .containsExactly(MetaSyntacticVariable.FOO, MetaSyntacticVariable.BAZ);
+
+        assertThat(StringConversions.<int[]> as(int[].class, "5,7,9")).containsExactly(5, 7, 9);
+    }
+
+    @Test
+    public void testCommaDelimitedCollectionConversion() {
+        assertThat(StringConversions.as(new TypeLiteral<Set<MetaSyntacticVariable>>() {}, "bar,foo"))
+            .containsExactly(MetaSyntacticVariable.FOO, MetaSyntacticVariable.BAR).isInstanceOf(EnumSet.class);
+
+        assertThat(StringConversions.as(new TypeLiteral<List<Integer>>() {}, "2,4,6,8,4")).containsExactly(2, 4, 6, 8,
+            4);
+
+        assertThat(StringConversions.as(new TypeLiteral<Collection<Integer>>() {}, "2,4,6,8,4")).containsExactly(2, 4,
+            6, 8, 4);
+
+        assertThat(StringConversions.as(new TypeLiteral<Set<Integer>>() {}, "2,4,6,8,4")).containsExactly(2, 4, 6, 8);
+
+        assertThat(StringConversions.as(new TypeLiteral<ArrayDeque<String>>() {}, "moe,larry,curly"))
+            .containsExactly("moe", "larry", "curly");
+    }
+}
diff --git a/src/tests/junit/org/apache/ant/s3/strings/ClassNamesTest.java b/src/tests/junit/org/apache/ant/s3/strings/ClassNamesTest.java
new file mode 100644
index 0000000..d4c1b67
--- /dev/null
+++ b/src/tests/junit/org/apache/ant/s3/strings/ClassNamesTest.java
@@ -0,0 +1,345 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.strings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.ant.s3.strings.ClassNames.Direction;
+import org.junit.Test;
+
+/**
+ * Unit test {@link ClassNames}.
+ */
+public class ClassNamesTest {
+    @Test
+    public void testEmpty() {
+        assertThat(ClassNames.empty()).isEmpty();
+    }
+
+    @Test
+    public void testExplicitString() {
+        assertThat(ClassNames.of("foo"))
+
+            .containsExactly("foo")
+
+            .containsExactly("foo");
+    }
+
+    @Test
+    public void testExplicitStrings() {
+        assertThat(ClassNames.of("foo", "bar", "baz"))
+
+            .containsExactly("foo", "bar", "baz")
+
+            .containsExactly("foo", "bar", "baz");
+    }
+
+    @Test
+    public void testComposite() {
+        assertThat(
+
+            ClassNames.of("foo", "bar").andThen(
+
+                ClassNames.of("baz", "blah")
+
+            )
+
+        )
+
+            .isInstanceOf(ClassNames.class)
+
+            .containsExactly("foo", "bar", "baz", "blah")
+
+            .containsExactly("foo", "bar", "baz", "blah");
+    }
+
+    @Test
+    public void testChainedComposite() {
+        assertThat(
+
+            ClassNames.of("foo").andThen(
+
+                ClassNames.of("bar").andThen(
+
+                    ClassNames.of("baz").andThen(
+
+                        ClassNames.of("blah")
+
+                    )
+
+                )
+
+            )
+
+        )
+
+            .isInstanceOf(ClassNames.class)
+
+            .containsExactly("foo", "bar", "baz", "blah")
+
+            .containsExactly("foo", "bar", "baz", "blah");
+    }
+
+    @Test
+    public void testSorted() {
+        final ClassNames msv = ClassNames.of("foo", "bar", "baz");
+
+        assertThat(msv.sorted())
+
+            .isInstanceOf(ClassNames.class)
+
+            .containsExactly("bar", "baz", "foo")
+
+            .containsExactly("bar", "baz", "foo");
+
+        assertThat(msv.sorted(Comparator.reverseOrder()))
+
+            .isInstanceOf(ClassNames.class)
+
+            .containsExactly("foo", "baz", "bar")
+
+            .containsExactly("foo", "baz", "bar");
+    }
+
+    @Test
+    public void testReverse() {
+        assertThat(ClassNames.of("foo", "bar", "baz").reverse())
+
+            .isInstanceOf(ClassNames.class)
+
+            .containsExactly("baz", "bar", "foo")
+
+            .containsExactly("baz", "bar", "foo");
+    }
+
+    @Test
+    public void testDistinct() {
+        assertThat(ClassNames.of("foo", "bar", "baz", "foo", "bar", "baz").distinct())
+
+            .isInstanceOf(ClassNames.class)
+
+            .containsExactly("foo", "bar", "baz");
+    }
+
+    @Test
+    public void testExplicitClassname() {
+        assertThat(ClassNames.of(String.class)).containsExactlyElementsOf(simpleNames(String.class));
+    }
+
+    @Test
+    public void testExplicitClassnames() {
+        assertThat(ClassNames.of(String.class, Object.class, List.class))
+            .containsExactlyElementsOf(simpleNames(String.class, Object.class, List.class));
+    }
+
+    @Test
+    public void testSegmentsFromLeft() {
+        final ClassNames base = ClassNames.of("FooBarBaz");
+
+        assertThat(base.segment(Direction.FROM_LEFT, 0)).isSameAs(base);
+
+        assertThat(base.segment(Direction.FROM_LEFT, 1))
+
+            .containsExactly("BarBaz")
+
+            .containsExactly("BarBaz");
+
+        assertThat(base.segment(Direction.FROM_LEFT, 2))
+
+            .containsExactly("Baz")
+
+            .containsExactly("Baz");
+
+        assertThat(base.segment(Direction.FROM_LEFT, 3)).isEmpty();
+
+        assertThat(base.segments(Direction.FROM_LEFT, 0, 1))
+
+            .containsExactly("FooBarBaz", "BarBaz")
+
+            .containsExactly("FooBarBaz", "BarBaz");
+
+        assertThat(base.segments(Direction.FROM_LEFT, 1, 2))
+
+            .containsExactly("BarBaz", "Baz")
+
+            .containsExactly("BarBaz", "Baz");
+
+        for (int max = 2; max < 6; max++) {
+            assertThat(base.segments(Direction.FROM_LEFT, 0, max))
+
+                .containsExactly("FooBarBaz", "BarBaz", "Baz")
+
+                .containsExactly("FooBarBaz", "BarBaz", "Baz");
+        }
+
+        assertThat(base.segments(Direction.FROM_LEFT))
+
+            .containsExactly("FooBarBaz", "BarBaz", "Baz")
+
+            .containsExactly("FooBarBaz", "BarBaz", "Baz");
+    }
+
+    @Test
+    public void testSegmentsFromRight() {
+        final ClassNames base = ClassNames.of("FooBarBaz");
+
+        assertThat(base.segment(Direction.FROM_RIGHT, 0)).isSameAs(base);
+
+        assertThat(base.segment(Direction.FROM_RIGHT, 1))
+
+            .containsExactly("FooBar")
+
+            .containsExactly("FooBar");
+
+        assertThat(base.segment(Direction.FROM_RIGHT, 2))
+
+            .containsExactly("Foo")
+
+            .containsExactly("Foo");
+
+        assertThat(base.segment(Direction.FROM_RIGHT, 3)).isEmpty();
+
+        assertThat(base.segments(Direction.FROM_RIGHT, 0, 1))
+
+            .containsExactly("FooBarBaz", "FooBar")
+
+            .containsExactly("FooBarBaz", "FooBar");
+
+        assertThat(base.segments(Direction.FROM_RIGHT, 1, 2))
+
+            .containsExactly("FooBar", "Foo")
+
+            .containsExactly("FooBar", "Foo");
+
+        for (int max = 2; max < 6; max++) {
+            assertThat(base.segments(Direction.FROM_RIGHT, 0, max))
+
+                .containsExactly("FooBarBaz", "FooBar", "Foo")
+
+                .containsExactly("FooBarBaz", "FooBar", "Foo");
+        }
+
+        assertThat(base.segments(Direction.FROM_RIGHT))
+
+            .containsExactly("FooBarBaz", "FooBar", "Foo")
+
+            .containsExactly("FooBarBaz", "FooBar", "Foo");
+    }
+
+    @Test
+    public void testSegmentsFromLeftWithMultipleRoots() {
+        final ClassNames base = ClassNames.of("FooBarBaz", "DoReMi");
+
+        assertThat(base.segment(Direction.FROM_LEFT, 0)).isSameAs(base);
+
+        assertThat(base.segment(Direction.FROM_LEFT, 1))
+
+            .containsExactly("BarBaz", "ReMi")
+
+            .containsExactly("BarBaz", "ReMi");
+
+        assertThat(base.segment(Direction.FROM_LEFT, 2))
+
+            .containsExactly("Baz", "Mi")
+
+            .containsExactly("Baz", "Mi");
+
+        assertThat(base.segment(Direction.FROM_LEFT, 3)).isEmpty();
+
+        assertThat(base.segments(Direction.FROM_LEFT, 0, 1))
+
+            .containsExactly("FooBarBaz", "BarBaz", "DoReMi", "ReMi")
+
+            .containsExactly("FooBarBaz", "BarBaz", "DoReMi", "ReMi");
+
+        assertThat(base.segments(Direction.FROM_LEFT, 1, 2))
+
+            .containsExactly("BarBaz", "Baz", "ReMi", "Mi")
+
+            .containsExactly("BarBaz", "Baz", "ReMi", "Mi");
+
+        for (int max = 2; max < 6; max++) {
+            assertThat(base.segments(Direction.FROM_LEFT, 0, max))
+
+                .containsExactly("FooBarBaz", "BarBaz", "Baz", "DoReMi", "ReMi", "Mi")
+
+                .containsExactly("FooBarBaz", "BarBaz", "Baz", "DoReMi", "ReMi", "Mi");
+        }
+
+        assertThat(base.segments(Direction.FROM_LEFT))
+
+            .containsExactly("FooBarBaz", "BarBaz", "Baz", "DoReMi", "ReMi", "Mi")
+
+            .containsExactly("FooBarBaz", "BarBaz", "Baz", "DoReMi", "ReMi", "Mi");
+    }
+
+    @Test
+    public void testSegmentsFromRightWithMultipleRoots() {
+        final ClassNames base = ClassNames.of("FooBarBaz", "DoReMi");
+
+        assertThat(base.segment(Direction.FROM_RIGHT, 0)).isSameAs(base);
+
+        assertThat(base.segment(Direction.FROM_RIGHT, 1))
+
+            .containsExactly("FooBar", "DoRe")
+
+            .containsExactly("FooBar", "DoRe");
+
+        assertThat(base.segment(Direction.FROM_RIGHT, 2))
+
+            .containsExactly("Foo", "Do")
+
+            .containsExactly("Foo", "Do");
+
+        assertThat(base.segment(Direction.FROM_RIGHT, 3)).isEmpty();
+
+        assertThat(base.segments(Direction.FROM_RIGHT, 0, 1))
+
+            .containsExactly("FooBarBaz", "FooBar", "DoReMi", "DoRe")
+
+            .containsExactly("FooBarBaz", "FooBar", "DoReMi", "DoRe");
+
+        assertThat(base.segments(Direction.FROM_RIGHT, 1, 2))
+
+            .containsExactly("FooBar", "Foo", "DoRe", "Do")
+
+            .containsExactly("FooBar", "Foo", "DoRe", "Do");
+
+        for (int max = 2; max < 6; max++) {
+            assertThat(base.segments(Direction.FROM_RIGHT, 0, max))
+
+                .containsExactly("FooBarBaz", "FooBar", "Foo", "DoReMi", "DoRe", "Do")
+
+                .containsExactly("FooBarBaz", "FooBar", "Foo", "DoReMi", "DoRe", "Do");
+        }
+
+        assertThat(base.segments(Direction.FROM_RIGHT))
+
+            .containsExactly("FooBarBaz", "FooBar", "Foo", "DoReMi", "DoRe", "Do")
+
+            .containsExactly("FooBarBaz", "FooBar", "Foo", "DoReMi", "DoRe", "Do");
+    }
+
+    private Iterable<String> simpleNames(Class<?>... clazzes) {
+        return Stream.of(clazzes).map(Class::getSimpleName).collect(Collectors.toList());
+    }
+}
diff --git a/src/tests/junit/org/apache/ant/s3/strings/PackageNamesTest.java b/src/tests/junit/org/apache/ant/s3/strings/PackageNamesTest.java
new file mode 100644
index 0000000..8fd62a5
--- /dev/null
+++ b/src/tests/junit/org/apache/ant/s3/strings/PackageNamesTest.java
@@ -0,0 +1,286 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.strings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.Test;
+
+/**
+ * Unit test {@link PackageNames}.
+ */
+public class PackageNamesTest {
+    @Test
+    public void testEmpty() {
+        assertThat(PackageNames.empty()).isEmpty();
+    }
+
+    @Test
+    public void testExplicitString() {
+        assertThat(PackageNames.of("foo"))
+
+            .containsExactly("foo")
+
+            .containsExactly("foo");
+    }
+
+    @Test
+    public void testExplicitStrings() {
+        assertThat(PackageNames.of("foo", "bar", "baz"))
+
+            .containsExactly("foo", "bar", "baz")
+
+            .containsExactly("foo", "bar", "baz");
+    }
+
+    @Test
+    public void testComposite() {
+        assertThat(
+
+            PackageNames.of("foo", "bar").andThen(
+
+                PackageNames.of("baz", "blah")
+
+            )
+
+        )
+
+            .isInstanceOf(PackageNames.class)
+
+            .containsExactly("foo", "bar", "baz", "blah")
+
+            .containsExactly("foo", "bar", "baz", "blah");
+    }
+
+    @Test
+    public void testChainedComposite() {
+        assertThat(
+
+            PackageNames.of("foo").andThen(
+
+                PackageNames.of("bar").andThen(
+
+                    PackageNames.of("baz").andThen(
+
+                        PackageNames.of("blah")
+
+                    )
+
+                )
+
+            )
+
+        )
+
+            .isInstanceOf(PackageNames.class)
+
+            .containsExactly("foo", "bar", "baz", "blah")
+
+            .containsExactly("foo", "bar", "baz", "blah");
+    }
+
+    @Test
+    public void testSorted() {
+        final PackageNames msv = PackageNames.of("foo", "bar", "baz");
+
+        assertThat(msv.sorted())
+
+            .isInstanceOf(PackageNames.class)
+
+            .containsExactly("bar", "baz", "foo")
+
+            .containsExactly("bar", "baz", "foo");
+
+        assertThat(msv.sorted(Comparator.reverseOrder()))
+
+            .isInstanceOf(PackageNames.class)
+
+            .containsExactly("foo", "baz", "bar")
+
+            .containsExactly("foo", "baz", "bar");
+    }
+
+    @Test
+    public void testReverse() {
+        assertThat(PackageNames.of("foo", "bar", "baz").reverse())
+
+            .isInstanceOf(PackageNames.class)
+
+            .containsExactly("baz", "bar", "foo")
+
+            .containsExactly("baz", "bar", "foo");
+    }
+
+    @Test
+    public void testDistinct() {
+        assertThat(PackageNames.of("foo", "bar", "baz", "foo", "bar", "baz").distinct())
+
+            .isInstanceOf(PackageNames.class)
+
+            .containsExactly("foo", "bar", "baz");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testAncestorInvalidMinGen() {
+        PackageNames.of("foo").ancestors(-1, 0);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testAncestorInvalidMaxGen() {
+        PackageNames.of("foo").ancestors(0, -1);
+    }
+
+    @Test
+    public void testAncestors() {
+        final PackageNames foo_bar_baz = PackageNames.of("foo.bar.baz");
+
+        assertThat(foo_bar_baz.ancestor(0))
+
+            .isSameAs(foo_bar_baz)
+
+            .containsExactly("foo.bar.baz")
+
+            .containsExactly("foo.bar.baz");
+
+        assertThat(foo_bar_baz.ancestor(1))
+
+            .containsExactly("foo.bar")
+
+            .containsExactly("foo.bar");
+
+        assertThat(foo_bar_baz.ancestor(2))
+
+            .containsExactly("foo")
+
+            .containsExactly("foo");
+
+        assertThat(foo_bar_baz.ancestor(3)).isEmpty();
+
+        assertThat(foo_bar_baz.ancestors(1, 2))
+
+            .containsExactly("foo.bar", "foo")
+
+            .containsExactly("foo.bar", "foo");
+
+        for (int max = 2; max < 5; max++) {
+            assertThat(foo_bar_baz.ancestors(0, max))
+
+                .containsExactly("foo.bar.baz", "foo.bar", "foo")
+
+                .containsExactly("foo.bar.baz", "foo.bar", "foo");
+        }
+
+        assertThat(foo_bar_baz.ancestors())
+
+            .containsExactly("foo.bar.baz", "foo.bar", "foo")
+
+            .containsExactly("foo.bar.baz", "foo.bar", "foo");
+    }
+
+    @Test
+    public void testAncestorsMultipleRoots() {
+        final PackageNames base = PackageNames.of("foo.bar.baz", "moe.larry.curly.shemp");
+
+        assertThat(base.ancestor(1))
+
+            .containsExactly("foo.bar", "moe.larry.curly")
+
+            .containsExactly("foo.bar", "moe.larry.curly");
+
+        assertThat(base.ancestor(2))
+
+            .containsExactly("foo", "moe.larry")
+
+            .containsExactly("foo", "moe.larry");
+
+        assertThat(base.ancestor(3))
+
+            .containsExactly("moe")
+
+            .containsExactly("moe");
+
+        assertThat(base.ancestor(4)).isEmpty();
+
+        assertThat(base.ancestors(1, 2))
+
+            .containsExactly("foo.bar", "foo", "moe.larry.curly", "moe.larry")
+
+            .containsExactly("foo.bar", "foo", "moe.larry.curly", "moe.larry");
+
+        assertThat(base.ancestors(1, 3))
+
+            .containsExactly("foo.bar", "foo", "moe.larry.curly", "moe.larry", "moe")
+
+            .containsExactly("foo.bar", "foo", "moe.larry.curly", "moe.larry", "moe");
+
+        assertThat(base.ancestors(2, 3))
+
+            .containsExactly("foo", "moe.larry", "moe")
+
+            .containsExactly("foo", "moe.larry", "moe");
+
+        for (int max = 3; max < 6; max++) {
+            assertThat(base.ancestors(0, max))
+
+                .containsExactly("foo.bar.baz", "foo.bar", "foo", "moe.larry.curly.shemp", "moe.larry.curly",
+                    "moe.larry", "moe")
+
+                .containsExactly("foo.bar.baz", "foo.bar", "foo", "moe.larry.curly.shemp", "moe.larry.curly",
+                    "moe.larry", "moe");
+        }
+
+        assertThat(base.ancestors())
+
+            .containsExactly("foo.bar.baz", "foo.bar", "foo", "moe.larry.curly.shemp", "moe.larry.curly", "moe.larry",
+                "moe")
+
+            .containsExactly("foo.bar.baz", "foo.bar", "foo", "moe.larry.curly.shemp", "moe.larry.curly", "moe.larry",
+                "moe");
+    }
+
+    @Test
+    public void testExplicitRootClass() {
+        final Iterable<String> expected = packageNames(String.class);
+
+        assertThat(PackageNames.of(String.class))
+
+            .containsExactlyElementsOf(expected)
+
+            .containsExactlyElementsOf(expected);
+    }
+
+    @Test
+    public void testExplicitRootClasses() {
+        final Iterable<String> expected = packageNames(String.class, List.class, Pattern.class);
+
+        assertThat(PackageNames.of(String.class, List.class, Pattern.class))
+
+            .containsExactlyElementsOf(expected)
+
+            .containsExactlyElementsOf(expected);
+    }
+
+    private Iterable<String> packageNames(Class<?>... clazzes) {
+        return Stream.of(clazzes).map(c -> c.getPackage().getName()).collect(Collectors.toList());
+    }
+}
diff --git a/src/tests/junit/org/apache/ant/s3/strings/StringsTest.java b/src/tests/junit/org/apache/ant/s3/strings/StringsTest.java
new file mode 100644
index 0000000..485be3a
--- /dev/null
+++ b/src/tests/junit/org/apache/ant/s3/strings/StringsTest.java
@@ -0,0 +1,114 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.ant.s3.strings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Comparator;
+
+import org.junit.Test;
+
+/**
+ * Unit test {@link Strings}.
+ */
+public class StringsTest {
+    @Test
+    public void testEmpty() {
+        assertThat(Strings.empty()).isEmpty();
+    }
+
+    @Test
+    public void testExplicitString() {
+        assertThat(Strings.of("foo"))
+
+            .containsExactly("foo")
+
+            .containsExactly("foo");
+    }
+
+    @Test
+    public void testExplicitStrings() {
+        assertThat(Strings.of("foo", "bar", "baz"))
+
+            .containsExactly("foo", "bar", "baz")
+
+            .containsExactly("foo", "bar", "baz");
+    }
+
+    @Test
+    public void testComposite() {
+        assertThat(Strings.of("foo", "bar").andThen(Strings.of("baz", "blah")))
+
+            .containsExactly("foo", "bar", "baz", "blah")
+
+            .containsExactly("foo", "bar", "baz", "blah");
+    }
+
+    @Test
+    public void testChainedComposite() {
+        assertThat(
+
+            Strings.of("foo").andThen(
+
+                Strings.of("bar").andThen(
+
+                    Strings.of("baz").andThen(
+
+                        Strings.of("blah")
+
+                    )
+
+                )
+
+            )
+
+        )
+
+            .containsExactly("foo", "bar", "baz", "blah")
+
+            .containsExactly("foo", "bar", "baz", "blah");
+    }
+
+    @Test
+    public void testSorted() {
+        final Strings msv = Strings.of("foo", "bar", "baz");
+
+        assertThat(msv.sorted())
+
+            .containsExactly("bar", "baz", "foo")
+
+            .containsExactly("bar", "baz", "foo");
+
+        assertThat(msv.sorted(Comparator.reverseOrder()))
+
+            .containsExactly("foo", "baz", "bar")
+
+            .containsExactly("foo", "baz", "bar");
+    }
+
+    @Test
+    public void testReverse() {
+        assertThat(Strings.of("foo", "bar", "baz").reverse()).containsExactly("baz", "bar", "foo");
+    }
+
+    @Test
+    public void testDistinct() {
+        assertThat(Strings.of("foo", "bar", "baz", "foo", "bar", "baz").distinct())
+
+            .containsExactly("foo", "bar", "baz");
+    }
+}

[ant-antlibs-s3] 01/07: enhancements

Posted by mb...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

mbenson pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-s3.git

commit 53caf517795e39155816952dba73b8748e265924
Author: Matt Benson <mb...@apache.org>
AuthorDate: Mon Mar 28 14:07:23 2022 -0500

    enhancements
---
 build.xml | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/build.xml b/build.xml
index c14e931..8a4ca0e 100644
--- a/build.xml
+++ b/build.xml
@@ -32,6 +32,8 @@ under the License.
 
   <import file="common/build.xml"/>
 
+  <property name="ivy.report.todir" location="${build}/ivyreports" />
+
   <target name="install-all" depends="install">
     <ivy:retrieve pattern="${ant.home}/lib/[artifact]-[revision].[ext]" conf="default" />
   </target>
@@ -178,4 +180,19 @@ under the License.
       println com.adobe.testing.s3mock.S3MockApplication.DEFAULT_HTTPS_PORT
     </groovy>
   </target>
+
+  <target name="clean-all" depends="clean">
+    <delete dir="${lib.dir}" />
+  </target>
+
+  <target name="resolve-all" depends="install-ivy,-no-resolve" if="with.ivy" unless="no.resolve">
+    <ivy:resolve file="ivy.xml" conf="*(public),test" />
+    <ivy:retrieve pattern="${lib.dir}/[conf]/[artifact].[ext]" sync="yes" />
+  </target>
+
+  <target name="ivy-report" depends="install-ivy,-no-resolve" if="with.ivy" unless="no.resolve">
+    <ivy:resolve file="ivy.xml" />
+    <ivy:report />
+  </target>
+
 </project>

[ant-antlibs-s3] 05/07: publish-local

Posted by mb...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

mbenson pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-s3.git

commit 3178af37652208320e2a0dd26eea29e21b2ce5e6
Author: Matt Benson <mb...@apache.org>
AuthorDate: Thu Mar 31 08:42:26 2022 -0500

    publish-local
---
 build.xml | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/build.xml b/build.xml
index 8a4ca0e..59d577f 100644
--- a/build.xml
+++ b/build.xml
@@ -190,6 +190,13 @@ under the License.
     <ivy:retrieve pattern="${lib.dir}/[conf]/[artifact].[ext]" sync="yes" />
   </target>
 
+  <target name="publish-local" depends="prepare-upload">
+    <ivy:publish resolver="local"
+                 haltonmissing="false"
+                 overwrite="true"
+                 artifactspattern="${build.javarepository}/[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]" />
+  </target>
+
   <target name="ivy-report" depends="install-ivy,-no-resolve" if="with.ivy" unless="no.resolve">
     <ivy:resolve file="ivy.xml" />
     <ivy:report />

[ant-antlibs-s3] 03/07: .gitignore

Posted by mb...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

mbenson pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-s3.git

commit b9846a71854d207ec20174b913615ae5d0828476
Author: Matt Benson <mb...@apache.org>
AuthorDate: Mon Mar 28 18:02:42 2022 -0500

    .gitignore
---
 .gitignore | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c2208a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/build
+/ivy
+/lib

[ant-antlibs-s3] 04/07: exclude prefixes where possible

Posted by mb...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

mbenson pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-s3.git

commit 233cd38966cd39050ee54f6f9389537755c21235
Author: Matt Benson <mb...@apache.org>
AuthorDate: Thu Mar 31 08:41:56 2022 -0500

    exclude prefixes where possible
---
 src/main/org/apache/ant/s3/S3Finder.java | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/main/org/apache/ant/s3/S3Finder.java b/src/main/org/apache/ant/s3/S3Finder.java
index 659312a..95698ea 100644
--- a/src/main/org/apache/ant/s3/S3Finder.java
+++ b/src/main/org/apache/ant/s3/S3Finder.java
@@ -122,6 +122,7 @@ class S3Finder implements Supplier<Optional<ObjectResource>> {
         final String prefix;
         final TokenizedPath path;
         final Set<TokenizedPattern> includes;
+        final Set<TokenizedPattern> excludes;
         final int maxDepth;
         final Iterator<CommonPrefix> prefixes;
         final Iterator<Atom<?>> contents;
@@ -135,11 +136,12 @@ class S3Finder implements Supplier<Optional<ObjectResource>> {
 
             path = finder.path(prefix.get());
             includes = finder.patterns.getLeft();
+            excludes = finder.patterns.getRight();
             maxDepth = includes.stream().mapToInt(
                 include -> include.containsPattern(SelectorUtils.DEEP_TREE_MATCH) ? Integer.MAX_VALUE : include.depth())
                 .max().orElse(Integer.MAX_VALUE);
 
-            if (includes.isEmpty()) {
+            if (includes.isEmpty() && excludes.isEmpty()) {
                 this.prefixes = prefixes.iterator();
             } else {
                 final int recurseDepth = path.depth() + (finder.includePrefixes ? 0 : 1);
@@ -161,7 +163,12 @@ class S3Finder implements Supplier<Optional<ObjectResource>> {
         }
 
         final boolean allowPrefix(CommonPrefix prefix) {
-            return includes.stream().anyMatch(p -> p.matchStartOf(finder.path(prefix.prefix()), finder.caseSensitive));
+            final TokenizedPath asPath = finder.path(prefix.prefix());
+            if (maxDepth == asPath.depth()
+                && excludes.stream().anyMatch(p -> p.matchPath(asPath, finder.caseSensitive))) {
+                return false;
+            }
+            return includes.stream().anyMatch(p -> p.matchStartOf(asPath, finder.caseSensitive));
         }
 
         final boolean allow(Atom<?> atom) {