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:50 UTC
[ant-antlibs-s3] 07/07: total overhaul of SDK building
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");
+ }
+}