diff --git a/.changes/next-release/bugfix-AmazonS3-36e9d26.json b/.changes/next-release/bugfix-AmazonS3-36e9d26.json new file mode 100644 index 000000000000..7409a9652bf6 --- /dev/null +++ b/.changes/next-release/bugfix-AmazonS3-36e9d26.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "Amazon S3", + "description": "Fixed bug prevent GetBucketBolicy from ever being successful using the asynchronous S3 client." +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/ExecutionInterceptorChain.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/ExecutionInterceptorChain.java index 4e7750524ba7..6df44afc08b1 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/ExecutionInterceptorChain.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/ExecutionInterceptorChain.java @@ -16,10 +16,14 @@ package software.amazon.awssdk.core.interceptor; import java.io.InputStream; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.function.Consumer; + +import org.reactivestreams.Publisher; + import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.core.SdkRequest; import software.amazon.awssdk.core.SdkResponse; @@ -134,10 +138,15 @@ public InterceptorContext modifyHttpResponse(InterceptorContext context, public InterceptorContext modifyAsyncHttpResponse(InterceptorContext context, ExecutionAttributes executionAttributes) { InterceptorContext result = context; + for (int i = interceptors.size() - 1; i >= 0; i--) { + ExecutionInterceptor interceptor = interceptors.get(i); + + Publisher newResponsePublisher = + interceptor.modifyAsyncHttpResponseContent(result, executionAttributes).orElse(null); + result = result.toBuilder() - .responsePublisher(interceptors.get(i).modifyAsyncHttpResponseContent(result, executionAttributes) - .orElse(null)) + .responsePublisher(newResponsePublisher) .build(); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/EnvelopeWrappedSdkPublisher.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/EnvelopeWrappedSdkPublisher.java new file mode 100644 index 000000000000..7cd8b6ced129 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/EnvelopeWrappedSdkPublisher.java @@ -0,0 +1,154 @@ +/* + * Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.internal.async; + +import java.util.function.BiFunction; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.async.SdkPublisher; + +/** + * Publisher implementation that wraps the content of another publisher in an envelope with an optional prefix (or + * header) and suffix (or footer). The prefix content will be prepended to the first published object from the + * wrapped publisher, and the suffix content will be published when the wrapped publisher signals completion. + *

+ * The envelope prefix will not be published until the wrapped publisher publishes something or is completed. + * The envelope suffix will not be published until the wrapped publisher is completed. + *

+ * This class can be used in an asynchronous interceptor in the AWS SDK to wrap content around the incoming + * bytestream from a response. + *

+ * A function must be supplied that can be used to concatenate the envelope content with the content being published by + * the wrapped publisher. Example usage: + * {@code + * Publisher wrappedPublisher = ContentEnvelopeWrappingPublisher.of(publisher, "S", "E", (s1, s2) -> s1 + s2); + * } + * If publisher publishes a single string "1", wrappedPublisher will publish "S1" (prepending the envelop prefix). If + * publisher then publishes a second string "2", wrappedPublisher will then publish "2" (no added content). If + * publisher then completes, wrappedPublisher will then publish "E" and then complete. + *

+ * WARNING: This publisher implementation does not comply with the complete flow spec (as it inserts data into the + * middle of a flow between a third-party publisher and subscriber rather than acting as a fully featured + * independent publisher) and therefore should only be used in a limited fashion when we have complete control over + * how data is being published to the publisher it wraps. + * + * @param The type of objects being published + */ + +@SdkInternalApi +public class EnvelopeWrappedSdkPublisher implements SdkPublisher { + private final Publisher wrappedPublisher; + private final T contentPrefix; + private final T contentSuffix; + private final BiFunction mergeContentFunction; + + private EnvelopeWrappedSdkPublisher(Publisher wrappedPublisher, + T contentPrefix, + T contentSuffix, + BiFunction mergeContentFunction) { + this.wrappedPublisher = wrappedPublisher; + this.contentPrefix = contentPrefix; + this.contentSuffix = contentSuffix; + this.mergeContentFunction = mergeContentFunction; + } + + /** + * Create a new publisher that wraps the content of an existing publisher. + * @param wrappedPublisher The publisher who's content will be wrapped. + * @param contentPrefix The content to be inserted in front of the wrapped content. + * @param contentSuffix The content to be inserted at the back of the wrapped content. + * @param mergeContentFunction A function that will be used to merge the inserted content into the wrapped content. + * @param The content type. + * @return A newly initialized instance of this class. + */ + public static EnvelopeWrappedSdkPublisher of(Publisher wrappedPublisher, + T contentPrefix, + T contentSuffix, + BiFunction mergeContentFunction) { + return new EnvelopeWrappedSdkPublisher<>(wrappedPublisher, contentPrefix, contentSuffix, mergeContentFunction); + } + + /** + * See {@link Publisher#subscribe(Subscriber)} + */ + @Override + public void subscribe(Subscriber subscriber) { + if (subscriber == null) { + throw new NullPointerException("subscriber must be non-null on call to subscribe()"); + } + + wrappedPublisher.subscribe(new ContentWrappedSubscriber(subscriber)); + } + + private class ContentWrappedSubscriber implements Subscriber { + private final Subscriber wrappedSubscriber; + private boolean prefixApplied = false; + private boolean suffixApplied = false; + + private ContentWrappedSubscriber(Subscriber wrappedSubscriber) { + this.wrappedSubscriber = wrappedSubscriber; + } + + @Override + public void onSubscribe(Subscription subscription) { + wrappedSubscriber.onSubscribe(subscription); + } + + @Override + public void onNext(T t) { + T contentToSend = t; + + if (!prefixApplied) { + prefixApplied = true; + + if (contentPrefix != null) { + contentToSend = mergeContentFunction.apply(contentPrefix, t); + } + } + + wrappedSubscriber.onNext(contentToSend); + } + + @Override + public void onError(Throwable throwable) { + wrappedSubscriber.onError(throwable); + } + + @Override + public void onComplete() { + try { + // Only transmit the close of the envelope once and only if the prefix has been previously sent. + if (!suffixApplied && prefixApplied) { + suffixApplied = true; + + if (contentSuffix != null) { + // TODO: This should respect the demand from the subscriber as technically an onComplete + // signal could be received even if there is no demand. We have minimized the impact of this + // by only making it applicable in situations where there data has already been transmitted + // over the stream. + wrappedSubscriber.onNext(contentSuffix); + } + } + } finally { + wrappedSubscriber.onComplete(); + } + } + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/SdkPublishers.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/SdkPublishers.java new file mode 100644 index 000000000000..a86e58616d2f --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/SdkPublishers.java @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.internal.async; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import org.reactivestreams.Publisher; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.async.SdkPublisher; + +/** + * Common implementations of {@link SdkPublisher} that are provided for convenience when building asynchronous + * interceptors to be used with specific clients. + */ +@SdkInternalApi +public final class SdkPublishers { + private SdkPublishers() { + } + + /** + * Constructs an {@link SdkPublisher} that wraps a {@link ByteBuffer} publisher and inserts additional content + * that wraps the published content like an envelope. This can be used when you want to transform the content of + * an asynchronous SDK response by putting it in an envelope. This publisher implementation does not comply with + * the complete flow spec (as it inserts data into the middle of a flow between a third-party publisher and + * subscriber rather than acting as a fully featured independent publisher) and therefore should only be used in a + * limited fashion when we have complete control over how data is being published to the publisher it wraps. + * @param publisher The underlying publisher to wrap the content of. + * @param envelopePrefix A string representing the content to be inserted as the head of the containing envelope. + * @param envelopeSuffix A string representing the content to be inserted as the tail of containing envelope. + * @return An {@link SdkPublisher} that wraps the provided publisher. + */ + public static SdkPublisher envelopeWrappedPublisher(Publisher publisher, + String envelopePrefix, + String envelopeSuffix) { + return EnvelopeWrappedSdkPublisher.of(publisher, + wrapUtf8(envelopePrefix), + wrapUtf8(envelopeSuffix), + SdkPublishers::concat); + } + + private static ByteBuffer wrapUtf8(String s) { + return ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8)); + } + + private static ByteBuffer concat(ByteBuffer b1, ByteBuffer b2) { + ByteBuffer result = ByteBuffer.allocate(b1.remaining() + b2.remaining()); + result.put(b1); + result.put(b2); + result.rewind(); + return result; + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/async/SdkPublishersTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/async/SdkPublishersTest.java new file mode 100644 index 000000000000..e6bc9ac9ac23 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/async/SdkPublishersTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.async; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import software.amazon.awssdk.core.internal.async.SdkPublishers; +import utils.FakePublisher; + +public class SdkPublishersTest { + @Test + public void envelopeWrappedPublisher() { + FakePublisher fakePublisher = new FakePublisher<>(); + Publisher wrappedPublisher = + SdkPublishers.envelopeWrappedPublisher(fakePublisher, "prefix:", ":suffix"); + + FakeByteBufferSubscriber fakeSubscriber = new FakeByteBufferSubscriber(); + wrappedPublisher.subscribe(fakeSubscriber); + fakePublisher.publish(ByteBuffer.wrap("content".getBytes(StandardCharsets.UTF_8))); + fakePublisher.complete(); + + assertThat(fakeSubscriber.recordedEvents()).containsExactly("prefix:content", ":suffix"); + } + + private final static class FakeByteBufferSubscriber implements Subscriber { + private final List recordedEvents = new ArrayList<>(); + + @Override + public void onSubscribe(Subscription s) { + + } + + @Override + public void onNext(ByteBuffer byteBuffer) { + String s = StandardCharsets.UTF_8.decode(byteBuffer).toString(); + recordedEvents.add(s); + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onComplete() { + + } + + public List recordedEvents() { + return this.recordedEvents; + } + } +} \ No newline at end of file diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/async/EnvelopeWrappedSdkPublisherTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/async/EnvelopeWrappedSdkPublisherTest.java new file mode 100644 index 000000000000..349d1ef3b8aa --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/async/EnvelopeWrappedSdkPublisherTest.java @@ -0,0 +1,355 @@ +/* + * Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.internal.async; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +import java.util.function.BiFunction; +import java.util.stream.IntStream; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.reactivestreams.Subscriber; + +import utils.FakePublisher; + +@RunWith(MockitoJUnitRunner.class) +public class EnvelopeWrappedSdkPublisherTest { + private static final BiFunction CONCAT_STRINGS = (s1, s2) -> s1 + s2; + + private final FakePublisher fakePublisher = new FakePublisher<>(); + + @Mock + private Subscriber mockSubscriber; + + @Test + public void noPrefixOrSuffix_noEvent() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, null, null, CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + @Test + public void noPrefixOrSuffix_singleEvent() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, null, null, CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test1"); + verify(mockSubscriber).onNext("test1"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + @Test + public void noPrefixOrSuffix_multipleEvents() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, null, null, CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test1"); + verify(mockSubscriber).onNext("test1"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test2"); + verify(mockSubscriber).onNext("test2"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + @Test + public void prefixOnly_noEvent() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, "test-prefix:", null, CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + + @Test + public void prefixOnly_singleEvent() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, "test-prefix:", null, CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test1"); + verify(mockSubscriber).onNext("test-prefix:test1"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + @Test + public void prefixOnly_multipleEvents() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, "test-prefix:", null, CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test1"); + verify(mockSubscriber).onNext("test-prefix:test1"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test2"); + verify(mockSubscriber).onNext("test2"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + @Test + public void suffixOnly_noEvent() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, null, ":test-suffix", CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + @Test + public void suffixOnly_singleEvent() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, null, ":test-suffix", CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test"); + verify(mockSubscriber).onNext("test"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onNext(":test-suffix"); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + @Test + public void suffixOnly_multipleEvent() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, null, ":test-suffix", CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test1"); + verify(mockSubscriber).onNext("test1"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test2"); + verify(mockSubscriber).onNext("test2"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onNext(":test-suffix"); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + @Test + public void prefixAndSuffix_noEvent() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, "test-prefix:", ":test-suffix", CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + @Test + public void prefixAndSuffix_singleEvent() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, "test-prefix:", ":test-suffix", CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test"); + verify(mockSubscriber).onNext("test-prefix:test"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onNext(":test-suffix"); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + @Test + public void prefixAndSuffix_multipleEvent() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, "test-prefix:", ":test-suffix", CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test1"); + verify(mockSubscriber).onNext("test-prefix:test1"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.publish("test2"); + verify(mockSubscriber).onNext("test2"); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + fakePublisher.complete(); + verify(mockSubscriber).onNext(":test-suffix"); + verify(mockSubscriber).onComplete(); + verifyNoMoreInteractions(mockSubscriber); + } + + @Test + public void onError() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, "test-prefix:", ":test-suffix", CONCAT_STRINGS); + + verifyZeroInteractions(mockSubscriber); + + contentWrappingPublisher.subscribe(mockSubscriber); + verify(mockSubscriber, never()).onNext(anyString()); + verify(mockSubscriber).onSubscribe(any()); + verifyNoMoreInteractions(mockSubscriber); + reset(mockSubscriber); + + RuntimeException exception = new RuntimeException("boom"); + fakePublisher.doThrow(exception); + verify(mockSubscriber).onError(exception); + } + + @Test + public void subscribe_nullSubscriber_throwsNpe() { + EnvelopeWrappedSdkPublisher contentWrappingPublisher = + EnvelopeWrappedSdkPublisher.of(fakePublisher, "test-prefix:", ":test-suffix", CONCAT_STRINGS); + + assertThatThrownBy(() -> contentWrappingPublisher.subscribe((Subscriber)null)) + .isInstanceOf(NullPointerException.class); + } +} \ No newline at end of file diff --git a/core/sdk-core/src/test/java/utils/FakePublisher.java b/core/sdk-core/src/test/java/utils/FakePublisher.java new file mode 100644 index 000000000000..ec804ec1025d --- /dev/null +++ b/core/sdk-core/src/test/java/utils/FakePublisher.java @@ -0,0 +1,54 @@ +/* + * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 utils; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +public class FakePublisher implements Publisher { + private Subscriber delegateSubscriber; + + @Override + public void subscribe(Subscriber subscriber) { + this.delegateSubscriber = subscriber; + this.delegateSubscriber.onSubscribe(new FakeSubscription()); + } + + public void publish(T str) { + this.delegateSubscriber.onNext(str); + } + + public void complete() { + this.delegateSubscriber.onComplete(); + } + + public void doThrow(Throwable t) { + this.delegateSubscriber.onError(t); + } + + private static final class FakeSubscription implements Subscription { + @Override + public void request(long n) { + + } + + @Override + public void cancel() { + + } + } +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/GetBucketPolicyInterceptor.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/GetBucketPolicyInterceptor.java index 5d326e64e5c5..261c82df0a76 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/GetBucketPolicyInterceptor.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/GetBucketPolicyInterceptor.java @@ -18,11 +18,17 @@ import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; import java.io.InputStream; +import java.nio.ByteBuffer; import java.util.Optional; +import java.util.function.Predicate; + +import org.reactivestreams.Publisher; + import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.interceptor.Context; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.internal.async.SdkPublishers; import software.amazon.awssdk.http.AbortableInputStream; import software.amazon.awssdk.services.s3.model.GetBucketPolicyRequest; import software.amazon.awssdk.utils.IoUtils; @@ -33,24 +39,38 @@ */ @SdkInternalApi public final class GetBucketPolicyInterceptor implements ExecutionInterceptor { + private static final String XML_ENVELOPE_PREFIX = ""; + + private static final Predicate INTERCEPTOR_CONTEXT_PREDICATE = + context -> context.request() instanceof GetBucketPolicyRequest && context.httpResponse().isSuccessful(); @Override public Optional modifyHttpResponseContent(Context.ModifyHttpResponse context, ExecutionAttributes executionAttributes) { - if (context.request() instanceof GetBucketPolicyRequest && context.httpResponse().isSuccessful()) { + if (INTERCEPTOR_CONTEXT_PREDICATE.test(context)) { String policy = context.responseBody() .map(r -> invokeSafely(() -> IoUtils.toUtf8String(r))) .orElse(null); if (policy != null) { - // Wrap in CDATA to deal with any escaping issues - String xml = String.format("" - + "", policy); + String xml = XML_ENVELOPE_PREFIX + policy + XML_ENVELOPE_SUFFIX; return Optional.of(AbortableInputStream.create(new StringInputStream(xml))); } } return context.responseBody(); } + + @Override + public Optional> modifyAsyncHttpResponseContent(Context.ModifyHttpResponse context, + ExecutionAttributes executionAttributes) { + if (INTERCEPTOR_CONTEXT_PREDICATE.test(context)) { + return context.responsePublisher().map( + body -> SdkPublishers.envelopeWrappedPublisher(body, XML_ENVELOPE_PREFIX, XML_ENVELOPE_SUFFIX)); + } + + return context.responsePublisher(); + } } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/GetBucketPolicyFunctionalTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/GetBucketPolicyFunctionalTest.java new file mode 100644 index 000000000000..38b21b0c6aff --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/GetBucketPolicyFunctionalTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.services.s3.functionaltests; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; +import java.util.concurrent.CompletionException; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; + +import org.junit.Rule; +import org.junit.Test; + +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3AsyncClientBuilder; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3ClientBuilder; +import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse; +import software.amazon.awssdk.services.s3.model.GetBucketPolicyResponse; +import software.amazon.awssdk.services.s3.model.S3Exception; + +public class GetBucketPolicyFunctionalTest { + private static final URI HTTP_LOCALHOST_URI = URI.create("http://localhost:8080/"); + private static final String EXAMPLE_BUCKET = "Example-Bucket"; + private static final String EXAMPLE_POLICY = + "{\"Version\":\"2012-10-17\",\"Id\":\"Policy1234\"," + + "\"Statement\":[{\"Sid\":\"Stmt1578431058575\",\"Effect\":\"Allow\"," + + "\"Principal\":{\"AWS\":\"arn:aws:iam::1234567890:root\"},\"Action\":\"s3:*\"," + + "\"Resource\":\"arn:aws:s3:::dummy-resource/*\"}]}"; + + @Rule + public WireMockRule wireMock = new WireMockRule(); + + private S3ClientBuilder getSyncClientBuilder() { + + return S3Client.builder() + .region(Region.US_EAST_1) + .endpointOverride(HTTP_LOCALHOST_URI) + .credentialsProvider( + StaticCredentialsProvider.create(AwsBasicCredentials.create("key", "secret"))); + } + + private S3AsyncClientBuilder getAsyncClientBuilder() { + return S3AsyncClient.builder() + .region(Region.US_EAST_1) + .endpointOverride(HTTP_LOCALHOST_URI) + .credentialsProvider( + StaticCredentialsProvider.create(AwsBasicCredentials.create("key", "secret"))); + + } + + @Test + public void getBucketPolicy_syncClient() { + stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200).withBody(EXAMPLE_POLICY))); + + S3Client s3Client = getSyncClientBuilder().build(); + + GetBucketPolicyResponse response = s3Client.getBucketPolicy(r -> r.bucket(EXAMPLE_BUCKET)); + assertThat(response.policy()).isEqualTo(EXAMPLE_POLICY); + } + + @Test + public void getBucketPolicy_asyncClient() { + stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200).withBody(EXAMPLE_POLICY))); + + S3AsyncClient s3Client = getAsyncClientBuilder().build(); + + GetBucketPolicyResponse response = s3Client.getBucketPolicy(r -> r.bucket(EXAMPLE_BUCKET)).join(); + assertThat(response.policy()).isEqualTo(EXAMPLE_POLICY); + } +} diff --git a/test/protocol-tests-core/src/main/java/software/amazon/awssdk/protocol/runners/ProtocolTestRunner.java b/test/protocol-tests-core/src/main/java/software/amazon/awssdk/protocol/runners/ProtocolTestRunner.java index c4afc2089054..02852b8349c1 100644 --- a/test/protocol-tests-core/src/main/java/software/amazon/awssdk/protocol/runners/ProtocolTestRunner.java +++ b/test/protocol-tests-core/src/main/java/software/amazon/awssdk/protocol/runners/ProtocolTestRunner.java @@ -23,7 +23,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; -import software.amazon.awssdk.core.util.IdempotentUtils; import software.amazon.awssdk.protocol.model.TestCase; import software.amazon.awssdk.protocol.reflect.ClientReflector; import software.amazon.awssdk.protocol.wiremock.WireMockUtils; @@ -48,7 +47,6 @@ public ProtocolTestRunner(String intermediateModelLocation) { this.clientReflector = new ClientReflector(model); this.marshallingTestRunner = new MarshallingTestRunner(model, clientReflector); this.unmarshallingTestRunner = new UnmarshallingTestRunner(model, clientReflector); - IdempotentUtils.setGenerator(() -> "00000000-0000-4000-8000-000000000000"); } private IntermediateModel loadModel(String intermedidateModelLocation) { diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/AwsJsonProtocolTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/AwsJsonProtocolTest.java index 96426d13871e..f6308bde3a65 100644 --- a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/AwsJsonProtocolTest.java +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/AwsJsonProtocolTest.java @@ -20,7 +20,7 @@ import software.amazon.awssdk.protocol.ProtocolTestSuiteLoader; import software.amazon.awssdk.protocol.runners.ProtocolTestRunner; -public class AwsJsonProtocolTest { +public class AwsJsonProtocolTest extends ProtocolTestBase { private static final ProtocolTestSuiteLoader TEST_SUITE_LOADER = new ProtocolTestSuiteLoader(); private static ProtocolTestRunner testRunner; diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/Ec2ProtocolTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/Ec2ProtocolTest.java index ddae28db3df1..1154762a19de 100644 --- a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/Ec2ProtocolTest.java +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/Ec2ProtocolTest.java @@ -20,7 +20,7 @@ import software.amazon.awssdk.protocol.ProtocolTestSuiteLoader; import software.amazon.awssdk.protocol.runners.ProtocolTestRunner; -public class Ec2ProtocolTest { +public class Ec2ProtocolTest extends ProtocolTestBase { private static final ProtocolTestSuiteLoader testSuiteLoader = new ProtocolTestSuiteLoader(); private static ProtocolTestRunner testRunner; diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/ProtocolTestBase.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/ProtocolTestBase.java new file mode 100644 index 000000000000..90ae06d56a35 --- /dev/null +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/ProtocolTestBase.java @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocol.tests; + +import org.junit.BeforeClass; + +import software.amazon.awssdk.core.util.IdempotentUtils; + +/** + * All protocol tests should extend this class to ensure that the idempotency generator is overridden before the + * client class is loaded and the generator is cached, otherwise some tests in this suite can break. + */ +public class ProtocolTestBase { + @BeforeClass + public static void overrideIdempotencyTokenGenerator() { + IdempotentUtils.setGenerator(() -> "00000000-0000-4000-8000-000000000000"); + } +} diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryMetadataTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryMetadataTest.java index e1411fcf7744..f2e0abe4fe52 100644 --- a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryMetadataTest.java +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryMetadataTest.java @@ -35,7 +35,7 @@ import software.amazon.awssdk.services.protocolquery.model.ProtocolQueryResponse; import software.amazon.awssdk.utils.builder.SdkBuilder; -public class QueryMetadataTest { +public class QueryMetadataTest extends ProtocolTestBase { private static final String REQUEST_ID = "abcd"; diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryProtocolTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryProtocolTest.java index cafe97b759f2..7d9853c8e4ce 100644 --- a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryProtocolTest.java +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryProtocolTest.java @@ -20,7 +20,7 @@ import software.amazon.awssdk.protocol.ProtocolTestSuiteLoader; import software.amazon.awssdk.protocol.runners.ProtocolTestRunner; -public class QueryProtocolTest { +public class QueryProtocolTest extends ProtocolTestBase { private static final ProtocolTestSuiteLoader testSuiteLoader = new ProtocolTestSuiteLoader(); private static ProtocolTestRunner testRunner; diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryRequestTransformTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryRequestTransformTest.java index 5be7f51a4460..178bd8a285a6 100644 --- a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryRequestTransformTest.java +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/QueryRequestTransformTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ import java.net.URI; import java.util.List; -public class QueryRequestTransformTest { +public class QueryRequestTransformTest extends ProtocolTestBase { @Rule public WireMockRule wireMock = new WireMockRule(0); diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestJsonProtocolTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestJsonProtocolTest.java index 18e129dbcef5..6ac5c7758102 100644 --- a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestJsonProtocolTest.java +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestJsonProtocolTest.java @@ -20,7 +20,7 @@ import software.amazon.awssdk.protocol.ProtocolTestSuiteLoader; import software.amazon.awssdk.protocol.runners.ProtocolTestRunner; -public class RestJsonProtocolTest { +public class RestJsonProtocolTest extends ProtocolTestBase { private static final ProtocolTestSuiteLoader testSuiteLoader = new ProtocolTestSuiteLoader(); private static ProtocolTestRunner testRunner; diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestXmlProtocolTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestXmlProtocolTest.java index 533fde35041c..776abeb1062a 100644 --- a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestXmlProtocolTest.java +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestXmlProtocolTest.java @@ -20,7 +20,7 @@ import software.amazon.awssdk.protocol.ProtocolTestSuiteLoader; import software.amazon.awssdk.protocol.runners.ProtocolTestRunner; -public class RestXmlProtocolTest { +public class RestXmlProtocolTest extends ProtocolTestBase { private static final ProtocolTestSuiteLoader testSuiteLoader = new ProtocolTestSuiteLoader(); private static ProtocolTestRunner testRunner;