diff --git a/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/testing/FakeStorageRpc.java b/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/testing/FakeStorageRpc.java index ffa916dd3ea2..c4645beb7b8d 100644 --- a/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/testing/FakeStorageRpc.java +++ b/google-cloud-contrib/google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/testing/FakeStorageRpc.java @@ -18,8 +18,10 @@ import com.google.api.services.storage.model.Bucket; import com.google.api.services.storage.model.BucketAccessControl; +import com.google.api.services.storage.model.Notification; import com.google.api.services.storage.model.ObjectAccessControl; import com.google.api.services.storage.model.Policy; +import com.google.api.services.storage.model.ServiceAccount; import com.google.api.services.storage.model.StorageObject; import com.google.api.services.storage.model.TestIamPermissionsResponse; import com.google.cloud.Tuple; @@ -462,4 +464,24 @@ public Policy setIamPolicy(String bucket, Policy policy, Map options) public TestIamPermissionsResponse testIamPermissions(String bucket, List permissions, Map options) { throw new UnsupportedOperationException(); } + + @Override + public boolean deleteNotification(String bucket, String notification) { + throw new UnsupportedOperationException(); + } + + @Override + public List listNotifications(String bucket) { + throw new UnsupportedOperationException(); + } + + @Override + public Notification createNotification(String bucket, Notification notification) { + throw new UnsupportedOperationException(); + } + + @Override + public ServiceAccount getServiceAccount(String projectId) { + return null; + } } diff --git a/google-cloud-notification/README.md b/google-cloud-notification/README.md new file mode 100644 index 000000000000..9b2a47986ec5 --- /dev/null +++ b/google-cloud-notification/README.md @@ -0,0 +1,77 @@ +Google Cloud Java Client for Cloud Pub/Sub Notifications +================================= + + +- [Product Documentation](https://cloud.google.com/storage/docs/pubsub-notifications) +- [Client Library Documentation](https://googlecloudplatform.github.io/google-cloud-java/latest/apidocs/index.html?com/google/cloud/notification/package-summary.html) + +Quickstart +---------- +If you are using Maven, add this to your pom.xml file +```xml + + com.google.cloud + google-cloud-notification + 0.30.0-alpha + +``` +If you are using Gradle, add this to your dependencies +```Groovy +compile 'com.google.cloud:google-cloud-notification:0.30.0-alpha' +``` +If you are using SBT, add this to your dependencies +```Scala +libraryDependencies += "com.google.cloud" % "google-cloud-notification" % "0.30.0-alpha" +``` + +Authentication +-------------- + +See the +[Authentication](https://github.com/GoogleCloudPlatform/google-cloud-java#authentication) +section in the base directory's README. + +About Google Cloud Pub/Sub Notifications +---------------------------- + +Cloud Pub/Sub Notifications sends information about changes to objects in your buckets to Google Cloud Pub/Sub, where the information is added to a Cloud Pub/Sub topic of your choice in the form of messages. For example, you can track objects that are created and deleted in your bucket. Each notification contains information describing both the event that triggered it and the object that changed. + +Troubleshooting +--------------- + +To get help, follow the instructions in the [shared Troubleshooting document](https://github.com/GoogleCloudPlatform/gcloud-common/blob/master/troubleshooting/readme.md#troubleshooting). + +Java Versions +------------- + +Java 7 or above is required for using this client. + +Versioning +---------- + +This library follows [Semantic Versioning](http://semver.org/). + +It is currently in major version zero (``0.y.z``), which means that anything +may change at any time and the public API should not be considered +stable. + +Contributing +------------ + +Contributions to this library are always welcome and highly encouraged. + +See `google-cloud`'s [CONTRIBUTING] documentation and the [shared documentation](https://github.com/GoogleCloudPlatform/gcloud-common/blob/master/contributing/readme.md#how-to-contribute-to-gcloud) for more information on how to get started. + +Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. See [Code of Conduct][code-of-conduct] for more information. + +License +------- + +Apache 2.0 - See [LICENSE] for more information. + + +[CONTRIBUTING]:https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/CONTRIBUTING.md +[code-of-conduct]:https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/CODE_OF_CONDUCT.md#contributor-code-of-conduct +[LICENSE]: https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/LICENSE +[cloud-platform]: https://cloud.google.com/ +[developers-console]:https://console.developers.google.com/ diff --git a/google-cloud-notification/pom.xml b/google-cloud-notification/pom.xml new file mode 100644 index 000000000000..1ad3adf9b3f5 --- /dev/null +++ b/google-cloud-notification/pom.xml @@ -0,0 +1,196 @@ + + + 4.0.0 + google-cloud-notification + 0.30.1-alpha-SNAPSHOT + jar + Google Cloud Pub/Sub Notifications for Google Cloud Storage + https://github.com/GoogleCloudPlatform/google-cloud-java/tree/master/google-cloud-notification + + + Java idiomatic client for Google Cloud Notification. + + + com.google.cloud + google-cloud-pom + 0.30.1-alpha-SNAPSHOT + + + google-cloud-notification + ${project.version} + + + + io.netty + netty-tcnative-boringssl-static + + + ${project.groupId} + google-cloud-core + + + ${project.groupId} + google-cloud-storage + + + ${project.groupId} + google-cloud-pubsub + + + ${project.groupId} + google-cloud-core-grpc + + + com.google.api + gax-grpc + + + com.google.api + api-common + + + com.google.api.grpc + proto-google-cloud-pubsub-v1 + + + com.google.api.grpc + grpc-google-cloud-pubsub-v1 + test + + + io.grpc + grpc-netty + + + io.grpc + grpc-stub + + + io.grpc + grpc-auth + + + ${project.groupId} + google-cloud-core + test-jar + test + + + junit + junit + test + + + org.mockito + mockito-all + 1.9.5 + test + + + + + doclint-java8-disable + + [1.8,) + + + + -Xdoclint:none + + + + + + + ./.. + true + + project.properties + + + + + + maven-antrun-plugin + 1.4 + + + process-resources + + + + + + + run + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0-alpha-2 + + + generate-resources + + write-project-properties + + + ${project.build.outputDirectory}/project.properties + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.9.1 + + + generate-sources + + add-source + + + + generated/src/main/java + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.3 + + + attach-javadocs + + jar + + + ${javadoc.opts} + + + + + + maven-compiler-plugin + 3.5.1 + + 1.7 + 1.7 + UTF-8 + -Xlint:unchecked + + + + + diff --git a/google-cloud-notification/src/main/java/com/google/cloud/notification/Notification.java b/google-cloud-notification/src/main/java/com/google/cloud/notification/Notification.java new file mode 100644 index 000000000000..3f4ca5596cf8 --- /dev/null +++ b/google-cloud-notification/src/main/java/com/google/cloud/notification/Notification.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Google Inc. 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. + * 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 com.google.cloud.notification; + +import com.google.api.core.BetaApi; +import com.google.cloud.storage.Storage; +import java.util.List; + +/** + * An interface for Pub/Sub Notifications in Google Cloud Storage. + * + *

This is a light wrapper around a Storage client. + * + * @see Google Cloud Pub/Sub + * Notifications for Storage + */ +public interface Notification { + + /* Create a Notifications client wrapper on top of a given a Storage client. */ + Notification create(Storage storage); + + /* Delete the specified notification on the specified bucket. + * + * @return true if the notification was deleted, or false if not. + */ + @BetaApi + boolean deleteNotification(String bucket, String notification); + + /* List the notifications that are present on a given bucket. */ + @BetaApi + List listNotifications(String bucket); + + /* Create a notification on a bucket. */ + @BetaApi + NotificationInfo createNotification(String bucket, NotificationInfo notification); +} diff --git a/google-cloud-notification/src/main/java/com/google/cloud/notification/NotificationImpl.java b/google-cloud-notification/src/main/java/com/google/cloud/notification/NotificationImpl.java new file mode 100644 index 000000000000..9a753f5eea83 --- /dev/null +++ b/google-cloud-notification/src/main/java/com/google/cloud/notification/NotificationImpl.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017 Google Inc. 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. + * 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 com.google.cloud.notification; + +import static com.google.cloud.BaseService.EXCEPTION_HANDLER; +import static com.google.cloud.RetryHelper.runWithRetries; + +import com.google.cloud.BaseService; +import com.google.cloud.RetryHelper.RetryHelperException; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import com.google.cloud.storage.StorageOptions; +import com.google.cloud.storage.spi.v1.StorageRpc; +import com.google.common.collect.Lists; +import java.util.List; +import java.util.concurrent.Callable; + +public class NotificationImpl implements Notification { + + private final Storage storage; + + private NotificationImpl(Storage storage) { + this.storage = storage; + } + + @Override + public Notification create(Storage storage) { + return new NotificationImpl(storage); + } + + public static class DefaultNotificationFactory { + public Notification create(Storage storage) { + return new NotificationImpl(storage); + } + } + + @Override + public boolean deleteNotification(final String bucket, final String notification) { + try { + return runWithRetries(new Callable() { + @Override + public Boolean call() { + return ((StorageRpc) storage.getOptions().getRpc()).deleteNotification(bucket, notification); + } + }, storage.getOptions().getRetrySettings(), EXCEPTION_HANDLER, storage.getOptions().getClock()); + } catch (RetryHelperException e) { + throw StorageException.translateAndThrow(e); + } + } + + @Override + public List listNotifications(final String bucket) { + try { + List answer = runWithRetries(new Callable>() { + @Override + public List call() { + return ((StorageRpc) storage.getOptions().getRpc()).listNotifications(bucket); + } + }, storage.getOptions().getRetrySettings(), EXCEPTION_HANDLER, storage.getOptions().getClock()); + if (answer == null) { + return null; + } + return Lists.transform(answer, NotificationInfo.FROM_PB_FUNCTION); + } catch (RetryHelperException e) { + throw StorageException.translateAndThrow(e); + } + } + + @Override + public NotificationInfo createNotification(final String bucket, NotificationInfo notification) { + final com.google.api.services.storage.model.Notification notificationPb = notification.toPb(); + try { + return NotificationInfo.fromPb(runWithRetries(new Callable() { + @Override + public com.google.api.services.storage.model.Notification call() { + return ((StorageRpc) storage.getOptions().getRpc()).createNotification(bucket, notificationPb); + } + }, storage.getOptions().getRetrySettings(), EXCEPTION_HANDLER, storage.getOptions().getClock())); + } catch (RetryHelperException e) { + throw StorageException.translateAndThrow(e); + } + } + +} diff --git a/google-cloud-notification/src/main/java/com/google/cloud/notification/NotificationInfo.java b/google-cloud-notification/src/main/java/com/google/cloud/notification/NotificationInfo.java new file mode 100644 index 000000000000..0a5cf2f23cde --- /dev/null +++ b/google-cloud-notification/src/main/java/com/google/cloud/notification/NotificationInfo.java @@ -0,0 +1,314 @@ +/* + * Copyright 2017 Google Inc. 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. + * 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 com.google.cloud.notification; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.services.storage.model.Notification; +import com.google.common.base.Function; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.pubsub.v1.TopicName; +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Google Storage Notification metadata; + * + * @see Concepts and + * Terminology + */ +public class NotificationInfo implements Serializable { + // TODO: Consider making this an AutoValue class. + + private static final long serialVersionUID = 5725883368559753810L; + + public enum PayloadFormat { + JSON_API_V1, + NONE + } + + static final Function FROM_PB_FUNCTION = + new Function() { + @Override + public NotificationInfo apply(Notification pb) { + return NotificationInfo.fromPb(pb); + } + }; + static final Function TO_PB_FUNCTION = + new Function() { + @Override + public Notification apply(NotificationInfo NotificationInfo) { + return NotificationInfo.toPb(); + } + }; + private final String generatedId; + private final TopicName topic; + private final List eventTypes; + private final Map customAttributes; + private final PayloadFormat payloadFormat; + private final String objectNamePrefix; + private final String etag; + private final String selfLink; + + public static final class Builder { + + private String generatedId; + private TopicName topic; + private List eventTypes; + private Map customAttributes; + private PayloadFormat payloadFormat; + private String objectNamePrefix; + private String etag; + private String selfLink; + + Builder(TopicName topic) { + this.topic = topic; + } + + Builder(NotificationInfo NotificationInfo) { + generatedId = NotificationInfo.generatedId; + etag = NotificationInfo.etag; + selfLink = NotificationInfo.selfLink; + topic = NotificationInfo.topic; + eventTypes = NotificationInfo.eventTypes; + customAttributes = NotificationInfo.customAttributes; + payloadFormat = NotificationInfo.payloadFormat; + objectNamePrefix = NotificationInfo.objectNamePrefix; + } + + Builder setGeneratedId(String generatedId) { + this.generatedId = generatedId; + return this; + } + + Builder setSelfLink(String selfLink) { + this.selfLink = selfLink; + return this; + } + + public Builder setTopic(TopicName topic) { + this.topic = topic; + return this; + } + + public Builder setPayloadFormat(PayloadFormat payloadFormat) { + this.payloadFormat = payloadFormat; + return this; + } + + /** GcpLaunchStage.Alpha */ + public Builder setObjectNamePrefix(String objectNamePrefix) { + this.objectNamePrefix = objectNamePrefix; + return this; + } + + public Builder setEventTypes(Iterable eventTypes) { + this.eventTypes = eventTypes != null ? ImmutableList.copyOf(eventTypes) : null; + return this; + } + + Builder setEtag(String etag) { + this.etag = etag; + return this; + } + + public Builder setCustomAttributes(Map customAttributes) { + this.customAttributes = customAttributes != null ? ImmutableMap.copyOf(customAttributes) : null; + return this; + } + + public NotificationInfo build() { + checkNotNull(topic); + return new NotificationInfo(this); + } + } + + NotificationInfo(Builder builder) { + generatedId = builder.generatedId; + etag = builder.etag; + selfLink = builder.selfLink; + topic = builder.topic; + eventTypes = builder.eventTypes; + customAttributes = builder.customAttributes; + payloadFormat = builder.payloadFormat; + objectNamePrefix = builder.objectNamePrefix; + } + + /** + * Returns the service-generated id for the notification. + */ + public String getGeneratedId() { + return generatedId; + } + + /** + * Returns the Cloud PubSub topic to which this subscription publishes. + */ + public TopicName getTopic() { + return topic; + } + + /** + * Returns the canonical URI of this topic as a string. + */ + public String getSelfLink() { + return selfLink; + } + + /** + * Returns the desired content of the Payload. + */ + public PayloadFormat getPayloadFormat() { + return payloadFormat; + } + + /** + * Returns the object name prefix for which this notification configuration applies. + */ + public String getObjectNamePrefix() { + return objectNamePrefix; + } + + /** + * Returns HTTP 1.1 Entity tag for the notification. + * + * @see Entity Tags + */ + public String getEtag() { + return etag; + } + + /** + * Returns the list of event types that this notification will apply to. + * If empty, notifications will be sent on all event types. + * + * @see + * Cross-Origin Resource Sharing (CORS) + */ + public List getEventTypes() { + return eventTypes; + } + + /** + * Returns the list of additional attributes to attach to each Cloud PubSub message published for\ + * this notification subscription. + * + * @see + * About Access Control Lists + */ + public Map getCustomAttributes() { + return customAttributes; + } + + /** + * Returns a builder for the current notification. + */ + public Builder toBuilder() { + return new Builder(this); + } + + @Override + public int hashCode() { + return Objects.hash(getTopic()); + } + + @Override + public boolean equals(Object obj) { + return obj == this + || obj != null + && obj.getClass().equals(NotificationInfo.class) + && Objects.equals(toPb(), ((NotificationInfo) obj).toPb()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("topic", getTopic()) + .toString(); + } + + Notification toPb() { + Notification notificationPb = new Notification(); + notificationPb.setId(generatedId); + notificationPb.setEtag(etag); + if (customAttributes != null) { + notificationPb.setCustomAttributes(customAttributes); + } + if (eventTypes != null) { + notificationPb.setEventTypes(eventTypes); + } + if (objectNamePrefix != null) { + notificationPb.setObjectNamePrefix(objectNamePrefix); + } + if (payloadFormat != null) { + notificationPb.setPayloadFormat(payloadFormat.toString()); + } else { + notificationPb.setPayloadFormat(PayloadFormat.NONE.toString()); + } + notificationPb.setSelfLink(selfLink); + notificationPb.setTopic(topic.toString()); + + return notificationPb; + } + + /** + * Creates a {@code NotificationInfo} object for the provided topic name. + */ + public static NotificationInfo of(TopicName topic) { + return newBuilder(topic).build(); + } + + /** + * Returns a {@code NotificationInfo} builder where the topic's name is set to the provided name. + */ + public static Builder newBuilder(TopicName topic) { + return new Builder(topic); + } + + static NotificationInfo fromPb(Notification notificationPb) { + Builder builder = newBuilder(TopicName.parse(notificationPb.getTopic())); + if (notificationPb.getId() != null) { + builder.setGeneratedId(notificationPb.getId()); + } + if (notificationPb.getEtag() != null) { + builder.setEtag(notificationPb.getEtag()); + } + if (notificationPb.getCustomAttributes() != null) { + builder.setCustomAttributes(notificationPb.getCustomAttributes()); + } + if (notificationPb.getSelfLink() != null) { + builder.setSelfLink(notificationPb.getSelfLink()); + } + if (notificationPb.getObjectNamePrefix() != null) { + builder.setObjectNamePrefix(notificationPb.getObjectNamePrefix()); + } + if (notificationPb.getTopic() != null) { + builder.setTopic(TopicName.parse(notificationPb.getTopic())); + } + if (notificationPb.getEventTypes() != null) { + builder.setEventTypes(notificationPb.getEventTypes()); + } + if (notificationPb.getPayloadFormat() != null) { + builder.setPayloadFormat(PayloadFormat.valueOf(notificationPb.getPayloadFormat())); + } + return builder.build(); + } +} diff --git a/google-cloud-notification/src/test/java/com/google/cloud/notification/NotificationInfoTest.java b/google-cloud-notification/src/test/java/com/google/cloud/notification/NotificationInfoTest.java new file mode 100644 index 000000000000..b6f014f95e22 --- /dev/null +++ b/google-cloud-notification/src/test/java/com/google/cloud/notification/NotificationInfoTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2017 Google Inc. 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. + * 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 com.google.cloud.notification; + +import static org.junit.Assert.assertEquals; + +import com.google.cloud.notification.NotificationInfo.PayloadFormat; +import com.google.common.collect.ImmutableList; + +import com.google.common.collect.ImmutableMap; +import com.google.pubsub.v1.TopicName; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +public class NotificationInfoTest { + + private static final String ETAG = "0xFF00"; + private static final String GENERATED_ID = "B/N:1"; + private static final String SELF_LINK = "http://storage/b/n"; + private static final List EVENT_TYPES = ImmutableList.of("OBJECT_FINALIZE", "OBJECT_METADATA_UPDATE"); + private static final String OBJECT_NAME_PREFIX = "index.html"; + private static final PayloadFormat PAYLOAD_FORMAT = PayloadFormat.JSON_API_V1; + private static final TopicName TOPIC = TopicName.create("myProject", "topic1"); + private static final Map CUSTOM_ATTRIBUTES = ImmutableMap.of("label1", "value1"); + private static final NotificationInfo NOTIFICATION_INFO = NotificationInfo.newBuilder(TOPIC) + .setEtag(ETAG) + .setCustomAttributes(CUSTOM_ATTRIBUTES) + .setSelfLink(SELF_LINK) + .setEventTypes(EVENT_TYPES) + .setObjectNamePrefix(OBJECT_NAME_PREFIX) + .setPayloadFormat(PAYLOAD_FORMAT) + .setGeneratedId(GENERATED_ID) + .build(); + + @Test + public void testToBuilder() { + compareBuckets(NOTIFICATION_INFO, NOTIFICATION_INFO.toBuilder().build()); + NotificationInfo bucketInfo = NOTIFICATION_INFO.toBuilder().setGeneratedId("id").build(); + assertEquals("id", bucketInfo.getGeneratedId()); + bucketInfo = bucketInfo.toBuilder().setGeneratedId(GENERATED_ID).build(); + compareBuckets(NOTIFICATION_INFO, bucketInfo); + } + + @Test + public void testToBuilderIncomplete() { + NotificationInfo incompleteBucketInfo = NotificationInfo.newBuilder(TopicName.create("myProject", "topic1")).build(); + compareBuckets(incompleteBucketInfo, incompleteBucketInfo.toBuilder().build()); + } + + @Test + public void testOf() { + NotificationInfo bucketInfo = NotificationInfo.of(TopicName.create("myProject", "topic1")); + assertEquals(TopicName.create("myProject", "topic1"), bucketInfo.getTopic()); + } + + @Test + public void testBuilder() { + assertEquals(ETAG, NOTIFICATION_INFO.getEtag()); + assertEquals(GENERATED_ID, NOTIFICATION_INFO.getGeneratedId()); + assertEquals(SELF_LINK, NOTIFICATION_INFO.getSelfLink()); + assertEquals(EVENT_TYPES, NOTIFICATION_INFO.getEventTypes()); + assertEquals(OBJECT_NAME_PREFIX, NOTIFICATION_INFO.getObjectNamePrefix()); + assertEquals(PAYLOAD_FORMAT, NOTIFICATION_INFO.getPayloadFormat()); + assertEquals(TOPIC, NOTIFICATION_INFO.getTopic()); + assertEquals(CUSTOM_ATTRIBUTES, NOTIFICATION_INFO.getCustomAttributes()); + } + + @Test + public void testToPbAndFromPb() { + compareBuckets(NOTIFICATION_INFO, NotificationInfo.fromPb(NOTIFICATION_INFO.toPb())); + NotificationInfo bucketInfo = NotificationInfo.of( + TopicName.create("myProject", "topic1")) + .toBuilder() + .setPayloadFormat(PayloadFormat.NONE) + .build(); + compareBuckets(bucketInfo, NotificationInfo.fromPb(bucketInfo.toPb())); + } + + private void compareBuckets(NotificationInfo expected, NotificationInfo value) { + assertEquals(expected, value); + assertEquals(expected.getGeneratedId(), value.getGeneratedId()); + assertEquals(expected.getCustomAttributes(), value.getCustomAttributes()); + assertEquals(expected.getEtag(), value.getEtag()); + assertEquals(expected.getSelfLink(), value.getSelfLink()); + assertEquals(expected.getEventTypes(), value.getEventTypes()); + assertEquals(expected.getObjectNamePrefix(), value.getObjectNamePrefix()); + assertEquals(expected.getPayloadFormat(), value.getPayloadFormat()); + assertEquals(expected.getTopic(), value.getTopic()); + } +} diff --git a/google-cloud-notification/src/test/java/com/google/cloud/notification/it/ITSystemTest.java b/google-cloud-notification/src/test/java/com/google/cloud/notification/it/ITSystemTest.java new file mode 100644 index 000000000000..863ffb2e4ebe --- /dev/null +++ b/google-cloud-notification/src/test/java/com/google/cloud/notification/it/ITSystemTest.java @@ -0,0 +1,134 @@ +/* + * Copyright 2017 Google Inc. 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. + * 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 com.google.cloud.notification.it; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.ServiceOptions; +import com.google.cloud.notification.Notification; +import com.google.cloud.notification.NotificationImpl.DefaultNotificationFactory; +import com.google.cloud.pubsub.v1.TopicAdminClient; +import com.google.cloud.storage.BucketInfo; +import com.google.cloud.notification.NotificationInfo; +import com.google.cloud.notification.NotificationInfo.PayloadFormat; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.testing.RemoteStorageHelper; +import com.google.iam.v1.Binding; +import com.google.iam.v1.Policy; +import com.google.pubsub.v1.TopicName; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; + +public class ITSystemTest { + + private static RemoteStorageHelper remoteStorageHelper; + private static TopicAdminClient topicAdminClient; + private static Notification notificationService; + private static Storage storageService; + + private static final Logger log = Logger.getLogger(ITSystemTest.class.getName()); + private static final String BUCKET = RemoteStorageHelper.generateBucketName(); + private static final String NAME_SUFFIX = UUID.randomUUID().toString(); + private static String projectId; + + @Rule + public Timeout globalTimeout = Timeout.seconds(300); + + @BeforeClass + public static void beforeClass() throws NoSuchAlgorithmException, InvalidKeySpecException, IOException { + remoteStorageHelper = RemoteStorageHelper.create(); + topicAdminClient = TopicAdminClient.create(); + storageService = remoteStorageHelper.getOptions().getService(); + notificationService = new DefaultNotificationFactory().create(remoteStorageHelper.getOptions().getService()); + storageService.create(BucketInfo.of(BUCKET)); + projectId = ServiceOptions.getDefaultProjectId(); + } + + @AfterClass + public static void afterClass() throws Exception { + topicAdminClient.close(); + if (notificationService != null) { + boolean wasDeleted = RemoteStorageHelper.forceDelete(storageService, BUCKET, 5, TimeUnit.SECONDS); + if (!wasDeleted && log.isLoggable(Level.WARNING)) { + log.log(Level.WARNING, "Deletion of bucket {0} timed out, bucket is not empty", BUCKET); + } + } + } + + private String formatForTest(String resourceName) { + return resourceName + "-" + NAME_SUFFIX; + } + + @Test + public void testNotifications() { + // Use Pubsub to create a Topic. + final TopicName topic = TopicName.create(projectId, formatForTest("testing-topic-foo")); + topicAdminClient.createTopic(topic); + + Policy policy = topicAdminClient.getIamPolicy(topic.toString()); + Binding binding = + Binding.newBuilder().setRole("roles/owner").addMembers("allAuthenticatedUsers").build(); + Policy newPolicy = + topicAdminClient.setIamPolicy( + topic.toString(), policy.toBuilder().addBindings(binding).build()); + assertTrue(newPolicy.getBindingsList().contains(binding)); + String permissionName = "pubsub.topics.get"; + List permissions = + topicAdminClient + .testIamPermissions(topic.toString(), Collections.singletonList(permissionName)) + .getPermissionsList(); + assertTrue(permissions.contains(permissionName)); + + // Use Storage API to create a Notification on that Topic. + NotificationInfo notification = notificationService.createNotification(BUCKET, NotificationInfo.of(topic)); + assertNotNull(notification); + List notifications = notificationService.listNotifications(BUCKET); + assertTrue(notifications.contains(notification)); + assertEquals(1, notifications.size()); + + NotificationInfo notification2 = notificationService.createNotification(BUCKET, + NotificationInfo.of(topic) + .toBuilder() + .setPayloadFormat(PayloadFormat.JSON_API_V1).build()); + assertEquals(topic, notification2.getTopic()); + notifications = notificationService.listNotifications(BUCKET); + assertTrue(notifications.contains(notification)); + assertTrue(notifications.contains(notification2)); + assertEquals(2, notifications.size()); + assertTrue(notificationService.deleteNotification(BUCKET, notification.getGeneratedId())); + assertTrue(notificationService.deleteNotification(BUCKET, notification2.getGeneratedId())); + assertNull(notificationService.listNotifications(BUCKET)); + + topicAdminClient.deleteTopic(topic); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ServiceAccount.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ServiceAccount.java new file mode 100644 index 000000000000..a0d0d0971627 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ServiceAccount.java @@ -0,0 +1,102 @@ +/* + * Copyright 2017 Google Inc. 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. + * 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 com.google.cloud.storage; + +import com.google.common.base.Function; +import com.google.common.base.MoreObjects; + +import java.io.Serializable; +import java.util.Objects; + +/** + * A service account, with its specified scopes, authorized for this instance. + * + * @see Authenticating from Google + * Cloud Storage + */ +public final class ServiceAccount implements Serializable { + + static final Function + FROM_PB_FUNCTION = + new Function() { + @Override + public ServiceAccount apply(com.google.api.services.storage.model.ServiceAccount pb) { + return ServiceAccount.fromPb(pb); + } + }; + static final Function + TO_PB_FUNCTION = + new Function() { + @Override + public com.google.api.services.storage.model.ServiceAccount apply( + ServiceAccount metadata) { + return metadata.toPb(); + } + }; + + private static final long serialVersionUID = 4199610694227857331L; + + private final String email; + + private ServiceAccount(String email) { + this.email = email; + } + + /** + * Returns the email address of the service account. + */ + public String getEmail() { + return email; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("email", email) + .toString(); + } + + @Override + public int hashCode() { + return Objects.hash(email); + } + + @Override + public boolean equals(Object obj) { + return obj == this + || obj instanceof ServiceAccount + && Objects.equals(toPb(), ((ServiceAccount) obj).toPb()); + } + + com.google.api.services.storage.model.ServiceAccount toPb() { + com.google.api.services.storage.model.ServiceAccount serviceAccountPb = + new com.google.api.services.storage.model.ServiceAccount(); + serviceAccountPb.setEmailAddress(email); + return serviceAccountPb; + } + + /** + * Returns a {@code ServiceAccount} object for the provided email. + */ + public static ServiceAccount of(String email) { + return new ServiceAccount(email); + } + + static ServiceAccount fromPb(com.google.api.services.storage.model.ServiceAccount accountPb) { + return new ServiceAccount(accountPb.getEmailAddress()); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java index d75301738299..da3f108e81fd 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java @@ -2627,4 +2627,19 @@ public static Builder newBuilder() { @BetaApi @GcpLaunchStage.Alpha List testIamPermissions(String bucket, List permissions, BucketSourceOption... options); + + /** + * Returns the service account associated with the given project. + * + *

Example of getting a service account. + *

 {@code
+   * String projectId = "test@gmail.com";
+   * ServiceAccount account = storage.getServiceAccount(projectId);
+   * }
+ * + * @param projectId the ID of the project for which the service account should be fetched. + * @return the service account associated with this project + * @throws StorageException upon failure + */ + ServiceAccount getServiceAccount(String projectId); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java index bec844ff9a76..617cc0a05c89 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java @@ -17,6 +17,7 @@ package com.google.cloud.storage; import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.core.InternalApi; import com.google.cloud.BaseServiceException; import com.google.cloud.RetryHelper.RetryHelperException; import com.google.cloud.http.BaseHttpServiceException; @@ -30,6 +31,7 @@ * @see Google Cloud * Storage error codes */ +@InternalApi public final class StorageException extends BaseHttpServiceException { // see: https://cloud.google.com/storage/docs/resumable-uploads-xml#practices @@ -66,7 +68,7 @@ public StorageException(GoogleJsonError error) { * * @throws StorageException when {@code ex} was caused by a {@code StorageException} */ - static StorageException translateAndThrow(RetryHelperException ex) { + public static StorageException translateAndThrow(RetryHelperException ex) { BaseServiceException.translate(ex); throw new StorageException(UNKNOWN_CODE, ex.getMessage(), ex.getCause()); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java index 77e4ef4805ee..006fcfcbedca 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java @@ -955,6 +955,21 @@ public Boolean apply(String permission) { } } + @Override + public ServiceAccount getServiceAccount(final String projectId) { + try { + com.google.api.services.storage.model.ServiceAccount answer = runWithRetries(new Callable() { + @Override + public com.google.api.services.storage.model.ServiceAccount call() { + return storageRpc.getServiceAccount(projectId); + } + }, getOptions().getRetrySettings(), EXCEPTION_HANDLER, getOptions().getClock()); + return answer == null ? null : ServiceAccount.fromPb(answer); + } catch (RetryHelperException e) { + throw StorageException.translateAndThrow(e); + } + } + private static void addToOptionMap(StorageRpc.Option option, T defaultValue, Map map) { addToOptionMap(option, option, defaultValue, map); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java index 80b29c75adfd..5336a1b53e28 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java @@ -46,9 +46,11 @@ import com.google.api.services.storage.model.Buckets; import com.google.api.services.storage.model.ComposeRequest; import com.google.api.services.storage.model.ComposeRequest.SourceObjects.ObjectPreconditions; +import com.google.api.services.storage.model.Notification; import com.google.api.services.storage.model.ObjectAccessControl; import com.google.api.services.storage.model.Objects; import com.google.api.services.storage.model.Policy; +import com.google.api.services.storage.model.ServiceAccount; import com.google.api.services.storage.model.StorageObject; import com.google.api.services.storage.model.TestIamPermissionsResponse; import com.google.cloud.BaseServiceException; @@ -898,4 +900,45 @@ public TestIamPermissionsResponse testIamPermissions(String bucket, List throw translate(ex); } } + + @Override + public boolean deleteNotification(String bucket, String notification) { + try { + storage.notifications().delete(bucket, notification).execute(); + return true; + } catch (IOException ex) { + StorageException serviceException = translate(ex); + if (serviceException.getCode() == HTTP_NOT_FOUND) { + return false; + } + throw serviceException; + } + } + + @Override + public List listNotifications(String bucket) { + try { + return storage.notifications().list(bucket).execute().getItems(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public Notification createNotification(String bucket, Notification notification) { + try { + return storage.notifications().insert(bucket, notification).execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public ServiceAccount getServiceAccount(String projectId) { + try { + return storage.projects().serviceAccount().get(projectId).execute(); + } catch (IOException ex) { + throw translate(ex); + } + } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java index 6b20c4473924..9308008074ec 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java @@ -19,8 +19,10 @@ import com.google.api.core.InternalApi; import com.google.api.services.storage.model.Bucket; import com.google.api.services.storage.model.BucketAccessControl; +import com.google.api.services.storage.model.Notification; import com.google.api.services.storage.model.ObjectAccessControl; import com.google.api.services.storage.model.Policy; +import com.google.api.services.storage.model.ServiceAccount; import com.google.api.services.storage.model.StorageObject; import com.google.api.services.storage.model.TestIamPermissionsResponse; import com.google.cloud.ServiceRpc; @@ -430,4 +432,36 @@ void write(String uploadId, byte[] toWrite, int toWriteOffset, long destOffset, * @throws StorageException upon failure */ TestIamPermissionsResponse testIamPermissions(String bucket, List permissions, Map options); + + /** + * Deletes the notification with the specified name on the specified object. + * + * @return {@code true} if the notification was deleted, {@code false} if it was not found + * @throws StorageException upon failure + */ + boolean deleteNotification(String bucket, String notification); + + /** + * List the notifications for the provided bucket. + * + * @return a list of {@link Notification} objects that exist on the bucket. + * @throws StorageException upon failure + */ + List listNotifications(String bucket); + + /** + * Creates a notification with the specified entity on the specified bucket. + * + * @return the notification that was created. + * @throws StorageException upon failure + */ + Notification createNotification(String bucket, Notification notification); + + /** + * Returns the service account associated with the given project. + * + * @return the ID of the project to fetch the service account for. + * @throws StorageException upon failure + */ + ServiceAccount getServiceAccount(String projectId); } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ServiceAccountTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ServiceAccountTest.java new file mode 100644 index 000000000000..844da808d9d6 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ServiceAccountTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Google Inc. 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. + * 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 com.google.cloud.storage; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ServiceAccountTest { + + private static final ServiceAccount SERVICE_ACCOUNT = + ServiceAccount.of("email"); + + @Test + public void testOf() { + compareServiceAccount(SERVICE_ACCOUNT, ServiceAccount.of("email")); + } + + @Test + public void testToAndFromPb() { + compareServiceAccount(SERVICE_ACCOUNT, ServiceAccount.fromPb(SERVICE_ACCOUNT.toPb())); + } + + public void compareServiceAccount(ServiceAccount expected, ServiceAccount value) { + assertEquals(expected, value); + assertEquals(expected.getEmail(), value.getEmail()); + assertEquals(expected.hashCode(), value.hashCode()); + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java index 0f159872939c..e4a41391e55d 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java @@ -258,6 +258,8 @@ public class StorageImplTest { .setEtag(POLICY_ETAG1) .build(); + private static final ServiceAccount SERVICE_ACCOUNT = ServiceAccount.of("test@google.com"); + private static final com.google.api.services.storage.model.Policy API_POLICY1 = new com.google.api.services.storage.model.Policy() .setBindings(ImmutableList.of( @@ -2174,6 +2176,16 @@ public void testTestIamPermissionsNonNull() { assertEquals(expectedPermissions, storage.testIamPermissions(BUCKET_NAME1, checkedPermissions)); } + @Test + public void testGetServiceAccount() { + EasyMock.expect(storageRpcMock.getServiceAccount("projectId")) + .andReturn(SERVICE_ACCOUNT.toPb()); + EasyMock.replay(storageRpcMock); + initializeService(); + ServiceAccount serviceAccount = storage.getServiceAccount("projectId"); + assertEquals(SERVICE_ACCOUNT, serviceAccount); + } + @Test public void testRetryableException() { BlobId blob = BlobId.of(BUCKET_NAME1, BLOB_NAME1); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java index a62550cd1bdc..3647a646735f 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java @@ -41,6 +41,7 @@ import com.google.cloud.storage.BucketInfo; import com.google.cloud.storage.CopyWriter; import com.google.cloud.storage.HttpMethod; +import com.google.cloud.storage.ServiceAccount; import com.google.cloud.storage.Storage; import com.google.cloud.storage.Storage.BlobField; import com.google.cloud.storage.Storage.BucketField; @@ -105,6 +106,7 @@ public class ITStorageTest { private static final byte[] COMPRESSED_CONTENT = BaseEncoding.base64() .decode("H4sIAAAAAAAAAPNIzcnJV3DPz0/PSVVwzskvTVEILskvSkxPVQQA/LySchsAAAA="); private static final Map BUCKET_LABELS = ImmutableMap.of("label1", "value1"); + private static final String SERVICE_ACCOUNT_EMAIL = "gcloud-devel@gs-project-accounts.iam.gserviceaccount.com"; @BeforeClass public static void beforeClass() throws NoSuchAlgorithmException, InvalidKeySpecException { @@ -1624,4 +1626,12 @@ public void testListBucketRequesterPaysFails() throws InterruptedException { assertNull(remoteBucket.getSelfLink()); } } + + @Test + public void testGetServiceAccount() throws InterruptedException { + String projectId = remoteStorageHelper.getOptions().getProjectId(); + ServiceAccount serviceAccount = storage.getServiceAccount(projectId); + assertNotNull(serviceAccount); + assertEquals(SERVICE_ACCOUNT_EMAIL, serviceAccount.getEmail()); + } }