Skip to content

Commit 0b1c256

Browse files
committed
Add awsGeneratedTags field to S3Bucket in s3-event-notifications
Amazon S3 event notifications delivered to SNS, SQS, and Lambda now include an `awsGeneratedTags` map on the `bucket` object when system tags are enabled on the source bucket. The Java SDK model did not round-trip this field, so consumers had to fall back to raw JSON parsing to read the tags. This change adds `Map<String, String> awsGeneratedTags` to `S3Bucket` and threads it through the reader and writer: * `S3Bucket`: adds `awsGeneratedTags` field with a getter, a new four-argument constructor, and updates `equals`/`hashCode`/ `toString`. The pre-existing three-argument constructor is preserved so existing callers are source- and behavior-compatible. Tags are defensively copied on construction and returned as an unmodifiable view. * `DefaultS3EventNotificationReader`: parses `awsGeneratedTags` as `Map<String, String>`. Absent field yields `null`. Null value entries are preserved. Non-string values throw `IllegalArgumentException` with a helpful message including the offending key and value. * `DefaultS3EventNotificationWriter`: emits `awsGeneratedTags` only when the map is non-null and non-empty, matching S3's server-side `@JsonInclude(NON_EMPTY)` behavior so round-tripped output is indistinguishable from what S3 emits. Also bumps `eventVersion` fixtures in the module test files from `2.4` to `2.5` to reflect the schema version emitted alongside this launch. The change is fully additive: * Older SDKs remain compatible with new payloads (unknown fields are ignored during parse). * Newer SDKs remain compatible with old payloads (`getAwsGeneratedTags()` returns `null` when the field is absent).
1 parent aabeb50 commit 0b1c256

8 files changed

Lines changed: 610 additions & 29 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "S3 Event Notification",
4+
"contributor": "",
5+
"description": "Added `awsGeneratedTags` field to `S3Bucket` in the S3 Event Notifications module. Amazon S3 emits AWS-generated system tags on the `bucket` portion of event notifications when system tags are enabled on the source bucket. SDK consumers on the SNS/SQS/Lambda delivery paths can now access these tags via `S3Bucket#getAwsGeneratedTags()`."
6+
}

services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/internal/DefaultS3EventNotificationReader.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.nio.charset.StandardCharsets;
2121
import java.time.Instant;
2222
import java.util.Collections;
23+
import java.util.LinkedHashMap;
2324
import java.util.List;
2425
import java.util.Map;
2526
import java.util.stream.Collectors;
@@ -242,7 +243,29 @@ private S3Bucket readBucket(JsonNode jsonNode) {
242243
String name = expectStringOrNull(bucketNode, "name");
243244
UserIdentity ownerIdentity = readOwnerIdentity(bucketNode.get("ownerIdentity"));
244245
String arn = expectStringOrNull(bucketNode, "arn");
245-
return new S3Bucket(name, ownerIdentity, arn);
246+
Map<String, String> awsGeneratedTags = readStringMapOrNull(bucketNode.get("awsGeneratedTags"), "awsGeneratedTags");
247+
return new S3Bucket(name, ownerIdentity, arn, awsGeneratedTags);
248+
}
249+
250+
private Map<String, String> readStringMapOrNull(JsonNode jsonNode, String name) {
251+
Map<String, JsonNode> mapNode = expectObjectOrNull(jsonNode, name);
252+
if (mapNode == null) {
253+
return null;
254+
}
255+
Map<String, String> result = new LinkedHashMap<>();
256+
mapNode.forEach((tagKey, tagValueNode) ->
257+
result.put(tagKey, readTagStringValueOrNull(name, tagKey, tagValueNode)));
258+
return result;
259+
}
260+
261+
private String readTagStringValueOrNull(String mapName, String tagKey, JsonNode tagValue) {
262+
if (isNull(tagValue)) {
263+
return null;
264+
}
265+
Validate.isTrue(tagValue.isString(),
266+
"The value of tag '%s' in '%s' was not a string, but was: %s",
267+
tagKey, mapName, tagValue);
268+
return tagValue.asString();
246269
}
247270

248271
private UserIdentity readOwnerIdentity(JsonNode jsonNode) {

services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/internal/DefaultS3EventNotificationWriter.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.time.Instant;
2020
import java.time.format.DateTimeFormatter;
2121
import java.util.List;
22+
import java.util.Map;
2223
import software.amazon.awssdk.annotations.SdkInternalApi;
2324
import software.amazon.awssdk.eventnotifications.s3.model.GlacierEventData;
2425
import software.amazon.awssdk.eventnotifications.s3.model.IntelligentTieringEventData;
@@ -227,6 +228,17 @@ private void writeS3Bucket(JsonWriter writer, S3Bucket bucket) {
227228
writer.writeEndObject();
228229
}
229230
writeStringField(writer, "arn", bucket.getArn());
231+
Map<String, String> awsGeneratedTags = bucket.getAwsGeneratedTags();
232+
if (awsGeneratedTags != null && !awsGeneratedTags.isEmpty()) {
233+
writeStringMap(writer, "awsGeneratedTags", awsGeneratedTags);
234+
}
235+
writer.writeEndObject();
236+
}
237+
238+
private void writeStringMap(JsonWriter writer, String field, Map<String, String> map) {
239+
writer.writeFieldName(field);
240+
writer.writeStartObject();
241+
map.forEach((k, v) -> writeStringField(writer, k, v));
230242
writer.writeEndObject();
231243
}
232244

services-custom/s3-event-notifications/src/main/java/software/amazon/awssdk/eventnotifications/s3/model/S3Bucket.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
package software.amazon.awssdk.eventnotifications.s3.model;
1717

1818

19+
import java.util.Collections;
20+
import java.util.LinkedHashMap;
21+
import java.util.Map;
1922
import java.util.Objects;
2023
import software.amazon.awssdk.annotations.SdkPublicApi;
2124
import software.amazon.awssdk.utils.ToString;
@@ -29,11 +32,19 @@ public class S3Bucket {
2932
private final String name;
3033
private final UserIdentity ownerIdentity;
3134
private final String arn;
35+
private final Map<String, String> awsGeneratedTags;
3236

3337
public S3Bucket(String name, UserIdentity ownerIdentity, String arn) {
38+
this(name, ownerIdentity, arn, null);
39+
}
40+
41+
public S3Bucket(String name, UserIdentity ownerIdentity, String arn, Map<String, String> awsGeneratedTags) {
3442
this.name = name;
3543
this.ownerIdentity = ownerIdentity;
3644
this.arn = arn;
45+
this.awsGeneratedTags = awsGeneratedTags == null
46+
? null
47+
: Collections.unmodifiableMap(new LinkedHashMap<>(awsGeneratedTags));
3748
}
3849

3950
/**
@@ -57,6 +68,14 @@ public String getArn() {
5768
return arn;
5869
}
5970

71+
/**
72+
* @return The AWS-generated system tags for the bucket, or {@code null} if not present in the event. The returned map
73+
* is unmodifiable.
74+
*/
75+
public Map<String, String> getAwsGeneratedTags() {
76+
return awsGeneratedTags;
77+
}
78+
6079
@Override
6180
public boolean equals(Object o) {
6281
if (this == o) {
@@ -74,23 +93,30 @@ public boolean equals(Object o) {
7493
if (!Objects.equals(ownerIdentity, s3Bucket.ownerIdentity)) {
7594
return false;
7695
}
77-
return Objects.equals(arn, s3Bucket.arn);
96+
if (!Objects.equals(arn, s3Bucket.arn)) {
97+
return false;
98+
}
99+
return Objects.equals(awsGeneratedTags, s3Bucket.awsGeneratedTags);
78100
}
79101

80102
@Override
81103
public int hashCode() {
82104
int result = name != null ? name.hashCode() : 0;
83105
result = 31 * result + (ownerIdentity != null ? ownerIdentity.hashCode() : 0);
84106
result = 31 * result + (arn != null ? arn.hashCode() : 0);
107+
result = 31 * result + (awsGeneratedTags != null ? awsGeneratedTags.hashCode() : 0);
85108
return result;
86109
}
87110

88111
@Override
89112
public String toString() {
90-
return ToString.builder("S3Bucket")
91-
.add("name", name)
92-
.add("ownerIdentity", ownerIdentity)
93-
.add("arn", arn)
94-
.build();
113+
ToString builder = ToString.builder("S3Bucket")
114+
.add("name", name)
115+
.add("ownerIdentity", ownerIdentity)
116+
.add("arn", arn);
117+
if (awsGeneratedTags != null) {
118+
builder.add("awsGeneratedTags", awsGeneratedTags);
119+
}
120+
return builder.build();
95121
}
96122
}

services-custom/s3-event-notifications/src/test/java/software/amazon/awssdk/eventnotifications/s3/internal/EqualsAndHashCodeTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515

1616
package software.amazon.awssdk.eventnotifications.s3.internal;
1717

18-
import nl.jqno.equalsverifier.Warning;
19-
2018
import nl.jqno.equalsverifier.EqualsVerifier;
19+
import nl.jqno.equalsverifier.Warning;
2120
import org.junit.jupiter.api.Test;
2221

2322
class EqualsAndHashCodeTest {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.eventnotifications.s3.model;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
21+
import java.util.LinkedHashMap;
22+
import java.util.Map;
23+
import org.junit.jupiter.api.Test;
24+
25+
class S3BucketTest {
26+
27+
@Test
28+
void awsGeneratedTags_nullInput_returnsNull() {
29+
S3Bucket bucket = new S3Bucket("name", new UserIdentity("p"), "arn", null);
30+
assertThat(bucket.getAwsGeneratedTags()).isNull();
31+
}
32+
33+
@Test
34+
void threeArgConstructor_leavesAwsGeneratedTagsNull() {
35+
S3Bucket bucket = new S3Bucket("name", new UserIdentity("p"), "arn");
36+
assertThat(bucket.getAwsGeneratedTags()).isNull();
37+
}
38+
39+
@Test
40+
void toString_omitsAwsGeneratedTags_whenNull() {
41+
S3Bucket bucket = new S3Bucket("mybucket", new UserIdentity("p"), "arn:aws:s3:::mybucket");
42+
assertThat(bucket.toString()).doesNotContain("awsGeneratedTags");
43+
}
44+
45+
@Test
46+
void toString_includesAwsGeneratedTags_whenPresent() {
47+
Map<String, String> tags = new LinkedHashMap<>();
48+
tags.put("aws:resource:owner", "123456789012");
49+
tags.put("aws:resource:region", "us-west-2");
50+
51+
S3Bucket bucket = new S3Bucket("mybucket", new UserIdentity("p"), "arn:aws:s3:::mybucket", tags);
52+
assertThat(bucket.toString())
53+
.contains("awsGeneratedTags={aws:resource:owner=123456789012, aws:resource:region=us-west-2}");
54+
}
55+
56+
@Test
57+
void awsGeneratedTags_isDefensivelyCopied() {
58+
Map<String, String> tags = new LinkedHashMap<>();
59+
tags.put("aws:resource:owner", "123456789012");
60+
61+
S3Bucket bucket = new S3Bucket("mybucket", new UserIdentity("p"), "arn:aws:s3:::mybucket", tags);
62+
63+
// Mutating the caller's map after construction must not affect the bucket.
64+
tags.put("aws:resource:region", "us-west-2");
65+
tags.remove("aws:resource:owner");
66+
67+
assertThat(bucket.getAwsGeneratedTags())
68+
.containsExactly(new java.util.AbstractMap.SimpleEntry<>("aws:resource:owner", "123456789012"));
69+
}
70+
71+
@Test
72+
void getAwsGeneratedTags_isUnmodifiable() {
73+
Map<String, String> tags = new LinkedHashMap<>();
74+
tags.put("aws:resource:owner", "123456789012");
75+
76+
S3Bucket bucket = new S3Bucket("mybucket", new UserIdentity("p"), "arn:aws:s3:::mybucket", tags);
77+
78+
Map<String, String> returned = bucket.getAwsGeneratedTags();
79+
assertThatThrownBy(() -> returned.put("aws:resource:region", "us-west-2"))
80+
.isInstanceOf(UnsupportedOperationException.class);
81+
}
82+
}

0 commit comments

Comments
 (0)