Skip to content

Commit d5d490c

Browse files
vadmestenitisht
authored andcommitted
Add support of listen bucket notification (#727)
Also, change header/query map type to follow HTTP spec One header in HTTP can hold several values. ``` Header: Val1 Header: Val2 ``` The same thing goes for query: `GET /path/?key=val1&key=val2` This is needed when implementing listen bucket notification, because this API accept multiple events under the same query name.
1 parent ce2a1f4 commit d5d490c

File tree

14 files changed

+538
-12
lines changed

14 files changed

+538
-12
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Minio Java SDK for Amazon S3 Compatible Cloud Storage,
3+
* (C) 2018 Minio, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package io.minio;
19+
20+
import io.minio.notification.NotificationInfo;
21+
22+
public interface BucketEventListener {
23+
24+
void updateEvent(NotificationInfo info);
25+
}
26+

api/src/main/java/io/minio/MinioClient.java

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
package io.minio;
1919

20+
import com.google.common.collect.HashMultimap;
21+
import com.google.common.collect.Multimap;
22+
import com.google.common.collect.Multimaps;
2023
import com.google.common.io.ByteStreams;
2124

2225
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -57,6 +60,8 @@
5760
import io.minio.messages.NotificationConfiguration;
5861
import io.minio.org.apache.commons.validator.routines.InetAddressValidator;
5962

63+
import io.minio.notification.NotificationInfo;
64+
6065
import okhttp3.HttpUrl;
6166
import okhttp3.OkHttpClient;
6267
import okhttp3.Request;
@@ -69,6 +74,8 @@
6974
import org.xmlpull.v1.XmlPullParserException;
7075
import org.xmlpull.v1.XmlPullParserFactory;
7176

77+
import com.fasterxml.jackson.databind.ObjectMapper;
78+
7279
import java.io.BufferedInputStream;
7380
import java.io.IOException;
7481
import java.io.InputStream;
@@ -815,8 +822,8 @@ public boolean verify(String hostname, SSLSession session) {
815822
* @param length Length of HTTP request body.
816823
*/
817824
private Request createRequest(Method method, String bucketName, String objectName,
818-
String region, Map<String,String> headerMap,
819-
Map<String,String> queryParamMap, final String contentType,
825+
String region, Multimap<String,String> headerMap,
826+
Multimap<String,String> queryParamMap, final String contentType,
820827
Object body, int length)
821828
throws InvalidBucketNameException, NoSuchAlgorithmException, InvalidKeyException, InsufficientDataException,
822829
IOException, InternalException {
@@ -870,7 +877,7 @@ private Request createRequest(Method method, String bucketName, String objectNam
870877
}
871878

872879
if (queryParamMap != null) {
873-
for (Map.Entry<String,String> entry : queryParamMap.entrySet()) {
880+
for (Map.Entry<String,String> entry : queryParamMap.entries()) {
874881
urlBuilder.addEncodedQueryParameter(S3Escaper.encode(entry.getKey()), S3Escaper.encode(entry.getValue()));
875882
}
876883
}
@@ -880,7 +887,7 @@ private Request createRequest(Method method, String bucketName, String objectNam
880887
Request.Builder requestBuilder = new Request.Builder();
881888
requestBuilder.url(url);
882889
if (headerMap != null) {
883-
for (Map.Entry<String,String> entry : headerMap.entrySet()) {
890+
for (Map.Entry<String,String> entry : headerMap.entries()) {
884891
requestBuilder.header(entry.getKey(), entry.getValue());
885892
}
886893
}
@@ -998,9 +1005,29 @@ private HttpResponse execute(Method method, String region, String bucketName, St
9981005
throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException,
9991006
InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException,
10001007
InternalException {
1001-
String contentType = null;
1008+
1009+
Multimap<String, String> queryParamMultiMap = null;
1010+
if (queryParamMap != null) {
1011+
queryParamMultiMap = Multimaps.forMap(queryParamMap);
1012+
}
1013+
1014+
Multimap<String, String> headerMultiMap = null;
10021015
if (headerMap != null) {
1003-
contentType = headerMap.get("Content-Type");
1016+
headerMultiMap = Multimaps.forMap(headerMap);
1017+
}
1018+
1019+
return executeReq(method, region, bucketName, objectName, headerMultiMap, queryParamMultiMap, body, length);
1020+
}
1021+
1022+
private HttpResponse executeReq(Method method, String region, String bucketName, String objectName,
1023+
Multimap<String,String> headerMap, Multimap<String,String> queryParamMap,
1024+
Object body, int length)
1025+
throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException,
1026+
InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException,
1027+
InternalException {
1028+
String contentType = null;
1029+
if (headerMap != null && headerMap.get("Content-Type") != null) {
1030+
contentType = String.join(" ", headerMap.get("Content-Type"));
10041031
}
10051032
if (body != null && !(body instanceof InputStream || body instanceof RandomAccessFile || body instanceof byte[])) {
10061033
byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
@@ -2053,8 +2080,16 @@ public String getPresignedObjectUrl(Method method, String bucketName, String obj
20532080
body = new byte[0];
20542081
}
20552082

2083+
Multimap<String, String> queryParamMap = null;
2084+
if (reqParams != null) {
2085+
queryParamMap = HashMultimap.create();
2086+
for (Map.Entry<String, String> m: reqParams.entrySet()) {
2087+
queryParamMap.put(m.getKey(), m.getValue());
2088+
}
2089+
}
2090+
20562091
String region = getRegion(bucketName);
2057-
Request request = createRequest(method, bucketName, objectName, region, null, reqParams, null, body, 0);
2092+
Request request = createRequest(method, bucketName, objectName, region, null, queryParamMap, null, body, 0);
20582093
HttpUrl url = Signer.presignV4(request, region, accessKey, secretKey, expires);
20592094
return url.toString();
20602095
}
@@ -4303,6 +4338,63 @@ public void removeIncompleteUpload(String bucketName, String objectName)
43034338
}
43044339
}
43054340

4341+
/**
4342+
* Listen to bucket notifications.
4343+
*
4344+
* @param bucketName Bucket name.
4345+
* @param prefix Prefix of concerned objects events.
4346+
* @param suffix Suffix of concerned objects events.
4347+
* @param events List of events to watch.
4348+
* @param eventCallback Event handler.
4349+
*
4350+
*/
4351+
4352+
public void listenBucketNotification(String bucketName, String prefix, String suffix, String[] events,
4353+
BucketEventListener eventCallback)
4354+
throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException,
4355+
InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException,
4356+
InternalException {
4357+
4358+
Multimap<String,String> queryParamMap = HashMultimap.create();
4359+
queryParamMap.put("prefix", prefix);
4360+
queryParamMap.put("suffix", suffix);
4361+
for (String event: events) {
4362+
queryParamMap.put("events", event);
4363+
}
4364+
4365+
String bodyContent = "";
4366+
Scanner scanner = null;
4367+
HttpResponse response = null;
4368+
ObjectMapper mapper = new ObjectMapper();
4369+
4370+
try {
4371+
response = executeReq(Method.GET, getRegion(bucketName),
4372+
bucketName, "", null, queryParamMap, null, 0);
4373+
scanner = new Scanner(response.body().charStream());
4374+
scanner.useDelimiter("\n");
4375+
while (scanner.hasNext()) {
4376+
bodyContent = scanner.next().trim();
4377+
if (bodyContent.equals("")) {
4378+
continue;
4379+
}
4380+
NotificationInfo ni = mapper.readValue(bodyContent, NotificationInfo.class);
4381+
eventCallback.updateEvent(ni);
4382+
}
4383+
} catch (RuntimeException e) {
4384+
throw e;
4385+
} catch (Exception e) {
4386+
throw e;
4387+
} finally {
4388+
if (response != null) {
4389+
response.body().close();
4390+
}
4391+
if (scanner != null) {
4392+
scanner.close();
4393+
}
4394+
}
4395+
}
4396+
4397+
43064398

43074399
/**
43084400
* Skips data of up to given length in given input stream.
@@ -4449,3 +4541,4 @@ public void traceOff() throws IOException {
44494541
this.traceStream = null;
44504542
}
44514543
}
4544+

api/src/main/java/io/minio/Signer.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@
2525
import java.util.Set;
2626
import java.util.TreeMap;
2727

28+
2829
import javax.crypto.Mac;
2930
import javax.crypto.spec.SecretKeySpec;
3031

3132
import org.joda.time.DateTime;
3233

3334
import com.google.common.base.Joiner;
35+
import com.google.common.collect.Multimap;
36+
import com.google.common.collect.MultimapBuilder;
3437
import com.google.common.io.BaseEncoding;
3538
import okhttp3.Headers;
3639
import okhttp3.HttpUrl;
@@ -142,16 +145,17 @@ private void setCanonicalHeaders() {
142145
this.signedHeaders = Joiner.on(";").join(this.canonicalHeaders.keySet());
143146
}
144147

145-
146148
private void setCanonicalQueryString() {
147-
Map<String,String> signedQueryParams = new TreeMap<>();
148-
149149
String encodedQuery = this.url.encodedQuery();
150150
if (encodedQuery == null) {
151151
this.canonicalQueryString = "";
152152
return;
153153
}
154154

155+
// Building a multimap which only order keys, ordering values is not performed
156+
// until Minio server supports it.
157+
Multimap<String, String> signedQueryParams = MultimapBuilder.treeKeys().arrayListValues().build();
158+
155159
for (String queryParam : encodedQuery.split("&")) {
156160
String[] tokens = queryParam.split("=");
157161
if (tokens.length > 1) {
@@ -161,10 +165,9 @@ private void setCanonicalQueryString() {
161165
}
162166
}
163167

164-
this.canonicalQueryString = Joiner.on("&").withKeyValueSeparator("=").join(signedQueryParams);
168+
this.canonicalQueryString = Joiner.on("&").withKeyValueSeparator("=").join(signedQueryParams.entries());
165169
}
166170

167-
168171
private void setCanonicalRequest() throws NoSuchAlgorithmException {
169172
setCanonicalHeaders();
170173
this.url = this.request.url();

api/src/main/java/io/minio/messages/EventType.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ public enum EventType {
3131
OBJECT_CREATED_POST("s3:ObjectCreated:Post"),
3232
OBJECT_CREATED_COPY("s3:ObjectCreated:Copy"),
3333
OBJECT_CREATED_COMPLETE_MULTIPART_UPLOAD("s3:ObjectCreated:CompleteMultipartUpload"),
34+
OBJECT_ACCESSED_GET("s3:ObjectAccessed:Get"),
35+
OBJECT_ACCESSED_HEAD("s3:ObjectAccessed:Head"),
36+
OBJECT_ACCESSED_ANY("s3:ObjectAccessed:*"),
3437
OBJECT_REMOVED_ANY("s3:ObjectRemoved:*"),
3538
OBJECT_REMOVED_DELETE("s3:ObjectRemoved:Delete"),
3639
OBJECT_REMOVED_DELETED_MARKER_CREATED("s3:ObjectRemoved:DeleteMarkerCreated"),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Minio Java SDK for Amazon S3 Compatible Cloud Storage,
3+
* (C) 2018 Minio, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package io.minio.notification;
19+
20+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
21+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
22+
23+
@SuppressFBWarnings("UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD")
24+
@JsonIgnoreProperties(ignoreUnknown = true)
25+
public class BucketMeta {
26+
public String name;
27+
public Identity ownerIdentity;
28+
public String arn;
29+
}
30+
31+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Minio Java SDK for Amazon S3 Compatible Cloud Storage,
3+
* (C) 2018 Minio, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package io.minio.notification;
19+
20+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
21+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
22+
23+
@SuppressFBWarnings("UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD")
24+
@JsonIgnoreProperties(ignoreUnknown = true)
25+
public class EventMeta {
26+
public String schemaVersion;
27+
public String configurationId;
28+
public BucketMeta bucket;
29+
public ObjectMeta object;
30+
}
31+
32+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Minio Java SDK for Amazon S3 Compatible Cloud Storage,
3+
* (C) 2018 Minio, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package io.minio.notification;
19+
20+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
21+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
22+
23+
@SuppressFBWarnings("UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD")
24+
@JsonIgnoreProperties(ignoreUnknown = true)
25+
public class Identity {
26+
public String principalId;
27+
}
28+
29+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Minio Java SDK for Amazon S3 Compatible Cloud Storage,
3+
* (C) 2018 Minio, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package io.minio.notification;
19+
20+
import java.util.Map;
21+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
22+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
23+
24+
@SuppressFBWarnings("UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD")
25+
@JsonIgnoreProperties(ignoreUnknown = true)
26+
public class NotificationEvent {
27+
public String eventVersion;
28+
public String eventSource;
29+
public String awsRegion;
30+
public String eventTime;
31+
public String eventName;
32+
public Identity userIdentity;
33+
public Map<String, String> requestParameters;
34+
public Map<String, String> responseElements;
35+
public EventMeta s3;
36+
public SourceInfo source;
37+
}
38+

0 commit comments

Comments
 (0)