diff --git a/.checkstyle b/.checkstyle index 5783bc0d77a1..2ad23d5136f6 100644 --- a/.checkstyle +++ b/.checkstyle @@ -1,7 +1,7 @@ - - - + + + diff --git a/.classpath b/.classpath deleted file mode 100644 index dc5384546f80..000000000000 --- a/.classpath +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.eclipse-pmd b/.eclipse-pmd index c14648afb674..5b4a4cb75170 100644 --- a/.eclipse-pmd +++ b/.eclipse-pmd @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3ae51aeeae93..355e4b02954e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,33 @@ -/target/ +.gitignore + +# Packages +dist +build +bin +var +sdist +target + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject +*.iml +.idea .settings +.DS_Store + +# Built documentation +docs/ + + +# Wheel directory used in Travis builds. +gcloud-java-wheels/ diff --git a/.project b/.project deleted file mode 100644 index 89b7470af643..000000000000 --- a/.project +++ /dev/null @@ -1,65 +0,0 @@ - - - git-demo - - - - - - org.eclipse.wst.common.project.facet.core.builder - - - - - org.eclipse.jdt.core.javabuilder - - - - - net.sf.eclipsecs.core.CheckstyleBuilder - - - - - edu.umd.cs.findbugs.plugin.eclipse.findbugsBuilder - - - - - edu.umd.cs.findbugs.plugin.eclipse.findbugsBuilder - - - - - org.eclipse.wst.validation.validationbuilder - - - - - ntut.csie.rleht.builder.RLBuilder - - - - - ch.acanda.eclipse.pmd.builder.PMDBuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.eclipse.jem.workbench.JavaEMFNature - org.eclipse.wst.common.modulecore.ModuleCoreNature - org.eclipse.jdt.core.javanature - org.eclipse.m2e.core.maven2Nature - net.sf.eclipsecs.core.CheckstyleNature - edu.umd.cs.findbugs.plugin.eclipse.findbugsNature - org.eclipse.wst.common.project.facet.core.nature - ntut.csie.rleht.builder.RLNature - ch.acanda.eclipse.pmd.builder.PMDNature - - diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000000..c46dd8ceeaea --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: java +jdk: +- oraclejdk8 +- oraclejdk7 +- openjdk7 +before_install: +- mvn clean +- git clone -b travis `git config --get remote.origin.url` target/travis +- cp target/travis/settings.xml ~/.m2/settings.xml +install: mvn install +script: mvn verify +branches: + only: + - master +after_success: +- mvn cobertura:cobertura coveralls:report +- mvn site --settings target/travis/settings.xml +env: + global: + - secure: bjyc4GJSP9850m6KSO2LiGKMJI/iFJ6dIDNrrZJHiokWUv8ID5+X7O04YtAFF+WrYyVDJ8Zs+uduAJaQ5NFesnhFjMMNTOaliYIBjpBgdZU0vgmsU0NzO35bu6wA5DAdI8AGUNCVwSZpOAMnj/80dbYbyFwBn2DWBZ3QwpV6J/I= + - secure: CUM2l73KFm7U4eDsUKkh1WyEUzF3v94Ltvs7MnKU9olE1dNp3YmRBL9Lqhx3hSDqm/xv0ETQsPy29Fs2+VFkhQQxSley6iS/4trr2fioTB680txfXo/zDdmGSP1q1/U40fv1S+jvuBRAhDV5W+8dhWOGtzMH0tJp/TszeDGlmCY= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000000..df4129b3ed6d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ +Contributing +============ + +1. **Please sign one of the contributor license agreements below.** +1. Fork the repo, develop and test your code changes, add docs. +1. Make sure that your commit messages clearly describe the changes. +1. Send a pull request. + + +Here are some guidelines for hacking on gcloud-java. + + +Using maven for build/test +-------------------------- +After you cloned the repository use Maven for building and running the tests. +Maven 3.0+ is required. + + +Adding Features +--------------- +In order to add a feature to gcloud-java: + +The feature must be fully documented using Javadoc and examples should be provided. +The feature must work fully on Java 7 and above. +The feature must not add unnecessary dependencies (where "unnecessary" is of course subjective, +but new dependencies should be discussed). + + +Coding Style +------------ +Maintain the coding style in the project and in particular the modified files. +Follow the Google Java [style](http://google-styleguide.googlecode.com/svn/trunk/javaguide.html). + + +## Contributor License Agreements + +Before we can accept your pull requests you'll need to sign a Contributor +License Agreement (CLA): + +- **If you are an individual writing original source code** and **you own the intellectual property**, +then you'll need to sign an [individual CLA][indvcla]. +- **If you work for a company that wants to allow you to contribute your work**, +then you'll need to sign a [corporate CLA][corpcla]. + +You can sign these electronically (just scroll to the bottom). After that, +we'll be able to accept your pull requests. + +[gcloudcli]: https://developers.google.com/cloud/sdk/gcloud/ +[indvcla]: https://developers.google.com/open-source/cla/individual +[corpcla]: https://developers.google.com/open-source/cla/corporate diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000000..4eedc0116add --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md index 157f813e551a..2baf37e639ad 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,128 @@ -git-demo -======== +Google Cloud Java Client +========================== + +Java idiomatic client for [Google Cloud Platform][cloud-platform] services. + +[![Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-java.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/gcloud-java) +[![Coverage Status](https://coveralls.io/repos/GoogleCloudPlatform/gcloud-java/badge.svg?branch=master)](https://coveralls.io/r/GoogleCloudPlatform/gcloud-java?branch=master) + +- [Homepage] (https://googlecloudplatform.github.io/gcloud-java/) +- [API Documentation] (http://googlecloudplatform.github.io/gcloud-java/apidocs) +- [Examples] (http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/examples/package-summary.html) + +This client supports the following Google Cloud Platform services: + +- [Google Cloud Datastore] (https://cloud.google.com/datastore/) + + + +> Note: This client is a work-in-progress, and may occasionally +> make backwards-incompatible changes. + +Quickstart +---------- +Add this to your pom.xml file +```xml + + com.google.gcloud + gcloud-java + LATEST + +``` + + + +Google Cloud Datastore +---------------------- + +Google [Cloud Datastore][cloud-datastore] is a fully managed, schemaless database for +storing non-relational data. Cloud Datastore automatically scales with +your users and supports ACID transactions, high availability of reads and +writes, strong consistency for reads and ancestor queries, and eventual +consistency for all other queries. + +See the [Google Cloud Datastore docs][cloud-datastore-activation] for more details on how to activate +Cloud Datastore for your project. + +See the ``gcloud-java`` API [datastore documentation][datastore-api] to learn how to interact +with the Cloud Datastore using this Client Library. + +```java +import com.google.gcloud.datastore.Datastore; +import com.google.gcloud.datastore.DatastoreFactory; +import com.google.gcloud.datastore.DatastoreOptions; +import com.google.gcloud.datastore.DateTime; +import com.google.gcloud.datastore.Entity; +import com.google.gcloud.datastore.Key; +import com.google.gcloud.datastore.KeyFactory; + +DatastoreOptions options = DatastoreOptions.builder().projectId(PROJECT_ID).build(); +Datastore datastore = DatastoreFactory.instance().get(options); +KeyFactory keyFactory = datastore.newKeyFactory().kind(KIND); +Key key = keyFactory.newKey(keyName); +Entity entity = datastore.get(key); +if (entity == null) { + entity = Entity.builder(key) + .set("name", "John Do") + .set("age", 30) + .set("access_time", DateTime.now()) + .build(); + datastore.put(entity); +} else { + System.out.println("Updating access_time for " + entity.getString("name")); + entity = Entity.builder(entity) + .set("access_time", DateTime.now()) + .build(); + datastore.update(entity); +} +``` + +Contributing +------------ + +Contributions to this library are always welcome and highly encouraged. + +See [CONTRIBUTING] for more information on how to get started. + +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. + +License +------- + +Apache 2.0 - See [LICENSE] for more information. + + +[CONTRIBUTING]:https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/CONTRIBUTING.md +[LICENSE]: https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/LICENSE +[cloud-platform]: https://cloud.google.com/ +[cloud-datastore]: https://cloud.google.com/datastore/docs +[cloud-datastore-docs]: https://cloud.google.com/datastore/docs +[cloud-datastore-activation]: https://cloud.google.com/datastore/docs/activate +[datastore-api]: http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/datastore/package-summary.html + +[cloud-pubsub]: https://cloud.google.com/pubsub/ +[cloud-pubsub-docs]: https://cloud.google.com/pubsub/docs + +[cloud-storage]: https://cloud.google.com/storage/ +[cloud-storage-docs]: https://cloud.google.com/storage/docs/overview +[cloud-storage-create-bucket]: https://cloud.google.com/storage/docs/cloud-console#_creatingbuckets diff --git a/RobustaSettings.xml b/RobustaSettings.xml deleted file mode 100644 index e957a402fb2a..000000000000 --- a/RobustaSettings.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/checkstyle.xml b/checkstyle.xml index 814915090b25..7b6d2abd5db8 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -1,171 +1,200 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + Checkstyle configuration that checks the Google coding conventions from: + + - Google Java Style + https://google-styleguide.googlecode.com/svn-history/r130/trunk/javaguide.html + + Checkstyle is very configurable. Be sure to read the documentation at + http://checkstyle.sf.net (or in your downloaded distribution). + + Most Checks are configurable, be sure to consult the documentation. + + To completely disable a check, just comment it out or delete it from the file. + + Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov. + + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - diff --git a/findbugs-exclude.xml b/findbugs-exclude.xml new file mode 100644 index 000000000000..43bc3321deb5 --- /dev/null +++ b/findbugs-exclude.xml @@ -0,0 +1,2 @@ + + diff --git a/full-pmd-ruleset.xml b/full-pmd-ruleset.xml deleted file mode 100644 index 9f1ec73cc736..000000000000 --- a/full-pmd-ruleset.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - Full 5.1.1 PMD rule set - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gcloud-java-core/README.md b/gcloud-java-core/README.md new file mode 100644 index 000000000000..aa8d4304f1fb --- /dev/null +++ b/gcloud-java-core/README.md @@ -0,0 +1,55 @@ +Google Cloud Java Client +========================== + +Java idiomatic client for [Google Cloud Platform][cloud-platform] services. + +[![Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-java.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/gcloud-java) +[![Coverage Status](https://coveralls.io/repos/GoogleCloudPlatform/gcloud-java/badge.svg?branch=master)](https://coveralls.io/r/GoogleCloudPlatform/gcloud-java?branch=master) + +- [Homepage] (https://googlecloudplatform.github.io/gcloud-java/) +- [API Documentation] (http://googlecloudplatform.github.io/gcloud-java/apidocs) +- [Examples] (http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/examples/package-summary.html) + +This module provides common functionality and is required by the other service specific modules. + +Quickstart +---------- +Add this to your pom.xml file +```xml + + com.google.gcloud + gcloud-java-core + LATEST + +``` + +Contributing +------------ + +Contributions to this library are always welcome and highly encouraged. + +See [CONTRIBUTING] for more information on how to get started. + +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. + +License +------- + +Apache 2.0 - See [LICENSE] for more information. + + +[CONTRIBUTING]:https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/CONTRIBUTING.md +[LICENSE]: https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/LICENSE +[cloud-platform]: https://cloud.google.com/ diff --git a/gcloud-java-core/pom.xml b/gcloud-java-core/pom.xml new file mode 100644 index 000000000000..fa2e1c18972f --- /dev/null +++ b/gcloud-java-core/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + com.google.gcloud + gcloud-java-core + jar + GCloud Java core + https://github.com/GoogleCloudPlatform/gcloud-java + + Core module for the gcloud-java. + + + com.google.gcloud + gcloud-java-pom + 0.0.5 + + + + com.google.auth + google-auth-library-credentials + 0.1.0 + + + com.google.auth + google-auth-library-oauth2-http + 0.1.0 + + + com.google.http-client + google-http-client + 1.20.0 + compile + + + com.google.oauth-client + google-oauth-client + 1.20.0 + compile + + + com.google.guava + guava + 18.0 + + + com.google.api-client + google-api-client-appengine + 1.20.0 + compile + + + guava-jdk5 + com.google.guava + + + + + com.google.http-client + google-http-client-jackson + 1.20.0 + compile + + + guava-jdk5 + com.google.guava + + + + + junit + junit + 4.12 + test + + + joda-time + joda-time + RELEASE + compile + + + org.json + json + 20090211 + compile + + + org.easymock + easymock + 3.3 + test + + + diff --git a/gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java b/gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java new file mode 100644 index 000000000000..6cdb737ddd91 --- /dev/null +++ b/gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java @@ -0,0 +1,214 @@ +/* + * Copyright 2015 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.gcloud; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.compute.ComputeCredential; +import com.google.api.client.googleapis.extensions.appengine.auth.oauth2.AppIdentityCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.jackson.JacksonFactory; +import com.google.auth.http.HttpCredentialsAdapter; +import com.google.auth.oauth2.GoogleCredentials; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.util.Objects; +import java.util.Set; + +/** + * Credentials for accessing Google Cloud services. + */ +public abstract class AuthCredentials implements Serializable { + + private static final long serialVersionUID = 236297804453464604L; + + private static class AppEngineAuthCredentials extends AuthCredentials { + + private static final long serialVersionUID = 7931300552744202954L; + + private static final AuthCredentials INSTANCE = new AppEngineAuthCredentials(); + + @Override + protected HttpRequestInitializer httpRequestInitializer(HttpTransport transport, + Set scopes) { + return new AppIdentityCredential(scopes); + } + + private Object readResolve() throws ObjectStreamException { + return INSTANCE; + } + } + + public static class ServiceAccountAuthCredentials extends AuthCredentials { + + private static final long serialVersionUID = 8007708734318445901L; + private final String account; + private final PrivateKey privateKey; + + private static final AuthCredentials NO_CREDENTIALS = new ServiceAccountAuthCredentials(); + + ServiceAccountAuthCredentials(String account, PrivateKey privateKey) { + this.account = checkNotNull(account); + this.privateKey = checkNotNull(privateKey); + } + + ServiceAccountAuthCredentials() { + account = null; + privateKey = null; + } + + @Override + protected HttpRequestInitializer httpRequestInitializer( + HttpTransport transport, Set scopes) { + GoogleCredential.Builder builder = new GoogleCredential.Builder() + .setTransport(transport) + .setJsonFactory(new JacksonFactory()); + if (privateKey != null) { + builder.setServiceAccountPrivateKey(privateKey); + builder.setServiceAccountId(account); + builder.setServiceAccountScopes(scopes); + } + return builder.build(); + } + + public String account() { + return account; + } + + public PrivateKey privateKey() { + return privateKey; + } + + @Override + public int hashCode() { + return Objects.hash(account, privateKey); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ServiceAccountAuthCredentials)) { + return false; + } + ServiceAccountAuthCredentials other = (ServiceAccountAuthCredentials) obj; + return Objects.equals(account, other.account) + && Objects.equals(privateKey, other.privateKey); + } + } + + private static class ComputeEngineAuthCredentials extends AuthCredentials { + + private static final long serialVersionUID = -5217355402127260144L; + + private transient ComputeCredential computeCredential; + + ComputeEngineAuthCredentials() throws IOException, GeneralSecurityException { + computeCredential = getComputeCredential(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + try { + computeCredential = getComputeCredential(); + } catch (GeneralSecurityException e) { + throw new IOException(e); + } + } + + @Override + protected HttpRequestInitializer httpRequestInitializer(HttpTransport transport, + Set scopes) { + return computeCredential; + } + } + + private static class ApplicationDefaultAuthCredentials extends AuthCredentials { + + private static final long serialVersionUID = -8306873864136099893L; + + private transient GoogleCredentials googleCredentials; + + ApplicationDefaultAuthCredentials() throws IOException { + googleCredentials = GoogleCredentials.getApplicationDefault(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + googleCredentials = GoogleCredentials.getApplicationDefault(); + } + + @Override + protected HttpRequestInitializer httpRequestInitializer(HttpTransport transport, + Set scopes) { + return new HttpCredentialsAdapter(googleCredentials); + } + } + + protected abstract HttpRequestInitializer httpRequestInitializer(HttpTransport transport, + Set scopes); + + public static AuthCredentials createForAppEngine() { + return AppEngineAuthCredentials.INSTANCE; + } + + public static AuthCredentials createForComputeEngine() + throws IOException, GeneralSecurityException { + return new ComputeEngineAuthCredentials(); + } + + /** + * Returns the Application Default Credentials. + * + *

+ * Returns the Application Default Credentials which are credentials that identify and authorize + * the whole application. This is the built-in service account if running on Google Compute Engine + * or the credentials file from the path in the environment variable + * GOOGLE_APPLICATION_CREDENTIALS. + *

+ * + * @return the credentials instance. + * @throws IOException if the credentials cannot be created in the current environment. + */ + public static AuthCredentials createApplicationDefaults() throws IOException { + return new ApplicationDefaultAuthCredentials(); + } + + public static ServiceAccountAuthCredentials createFor(String account, PrivateKey privateKey) { + return new ServiceAccountAuthCredentials(account, privateKey); + } + + public static AuthCredentials noCredentials() { + return ServiceAccountAuthCredentials.NO_CREDENTIALS; + } + + static ComputeCredential getComputeCredential() throws IOException, GeneralSecurityException { + NetHttpTransport transport = GoogleNetHttpTransport.newTrustedTransport(); + // Try to connect using Google Compute Engine service account credentials. + ComputeCredential credential = new ComputeCredential(transport, new JacksonFactory()); + // Force token refresh to detect if we are running on Google Compute Engine. + credential.refreshToken(); + return credential; + } +} diff --git a/gcloud-java-core/src/main/java/com/google/gcloud/BaseService.java b/gcloud-java-core/src/main/java/com/google/gcloud/BaseService.java new file mode 100644 index 000000000000..4d96ac5b86e6 --- /dev/null +++ b/gcloud-java-core/src/main/java/com/google/gcloud/BaseService.java @@ -0,0 +1 @@ +/* * Copyright 2015 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.gcloud; public abstract class BaseService implements Service { private final O options; protected BaseService(O options) { this.options = options; } @Override public O options() { return options; } } \ No newline at end of file diff --git a/src/main/java/com/google/gcloud/ExceptionHandler.java b/gcloud-java-core/src/main/java/com/google/gcloud/ExceptionHandler.java similarity index 74% rename from src/main/java/com/google/gcloud/ExceptionHandler.java rename to gcloud-java-core/src/main/java/com/google/gcloud/ExceptionHandler.java index c3b558e58100..412462ae156e 100644 --- a/src/main/java/com/google/gcloud/ExceptionHandler.java +++ b/gcloud-java-core/src/main/java/com/google/gcloud/ExceptionHandler.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 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.gcloud; import static com.google.common.base.MoreObjects.firstNonNull; @@ -27,18 +43,17 @@ public final class ExceptionHandler implements Serializable { private final ImmutableList interceptors; private final ImmutableSet> retriableExceptions; private final ImmutableSet> nonRetriableExceptions; - private final Set retryInfos = Sets.newHashSet(); + private final Set retryInfo = Sets.newHashSet(); public interface Interceptor extends Serializable { enum RetryResult { - RETRY(true), - ABORT(false); + RETRY(true), ABORT(false); private final boolean booleanValue; - private RetryResult(boolean booleanValue) { + RetryResult(boolean booleanValue) { this.booleanValue = booleanValue; } @@ -51,22 +66,22 @@ boolean booleanValue() { * This method is called before exception evaluation and could short-circuit the process. * * @param exception the exception that is being evaluated - * @return {@link RetryResult} to indicate if the exception should be ignored - * ({@link RetryResult#RETRY}), propagated ({@link RetryResult#ABORT}), - * or evaluation should proceed ({@code null}). + * @return {@link RetryResult} to indicate if the exception should be ignored ( + * {@link RetryResult#RETRY}), propagated ({@link RetryResult#ABORT}), or evaluation + * should proceed ({@code null}). */ - RetryResult shouldRetry(Exception exception); + RetryResult beforeEval(Exception exception); /** * This method is called after the evaluation and could alter its result. * * @param exception the exception that is being evaluated * @param retryResult the result of the evaluation so far. - * @return {@link RetryResult} to indicate if the exception should be ignored - * ({@link RetryResult#RETRY}), propagated ({@link RetryResult#ABORT}), - * or evaluation should proceed ({@code null}). + * @return {@link RetryResult} to indicate if the exception should be ignored ( + * {@link RetryResult#RETRY}), propagated ({@link RetryResult#ABORT}), or evaluation + * should proceed ({@code null}). */ - RetryResult shouldRetry(Exception exception, RetryResult retryResult); + RetryResult afterEval(Exception exception, RetryResult retryResult); } /** @@ -80,14 +95,12 @@ public static class Builder { private final ImmutableSet.Builder> nonRetriableExceptions = ImmutableSet.builder(); - private Builder() { - } + private Builder() {} /** - * Adds the exception handler interceptors. - * Call order will be maintained. - + * Adds the exception handler interceptors. Call order will be maintained. + * * @param interceptors the interceptors for this exception handler * @return the Builder for chaining */ @@ -171,35 +184,34 @@ private ExceptionHandler(Builder builder) { nonRetriableExceptions = builder.nonRetriableExceptions.build(); Preconditions.checkArgument( Sets.intersection(retriableExceptions, nonRetriableExceptions).isEmpty(), - "Same exception was found in both retriable and non-retriable sets"); + "Same exception was found in both retryable and non-retryable sets"); for (Class exception : retriableExceptions) { - addToRetryInfos(retryInfos, new RetryInfo(exception, Interceptor.RetryResult.RETRY)); + addRetryInfo(new RetryInfo(exception, Interceptor.RetryResult.RETRY), retryInfo); } for (Class exception : nonRetriableExceptions) { - addToRetryInfos(retryInfos, new RetryInfo(exception, Interceptor.RetryResult.ABORT)); + addRetryInfo(new RetryInfo(exception, Interceptor.RetryResult.ABORT), retryInfo); } } - private static void addToRetryInfos(Set retryInfos, RetryInfo retryInfo) { - for (RetryInfo current : retryInfos) { + private static void addRetryInfo(RetryInfo retryInfo, Set dest) { + for (RetryInfo current : dest) { if (current.exception.isAssignableFrom(retryInfo.exception)) { - addToRetryInfos(current.children, retryInfo); + addRetryInfo(retryInfo, current.children); return; } if (retryInfo.exception.isAssignableFrom(current.exception)) { retryInfo.children.add(current); } } - retryInfos.removeAll(retryInfo.children); - retryInfos.add(retryInfo); + dest.removeAll(retryInfo.children); + dest.add(retryInfo); } - - private static RetryInfo findMostSpecificRetryInfo(Set retryInfos, + private static RetryInfo findMostSpecificRetryInfo(Set retryInfo, Class exception) { - for (RetryInfo current : retryInfos) { + for (RetryInfo current : retryInfo) { if (current.exception.isAssignableFrom(exception)) { - RetryInfo match = findMostSpecificRetryInfo(current.children, exception); + RetryInfo match = findMostSpecificRetryInfo(current.children, exception); return match == null ? current : match; } } @@ -215,7 +227,7 @@ private static Method getCallableMethod(Class clazz) { return getCallableMethod(clazz.getSuperclass()); } catch (SecurityException e) { // This should never happen - throw new RuntimeException("Unexpected exception", e); + throw new IllegalStateException("Unexpected exception", e); } } @@ -223,10 +235,10 @@ void verifyCaller(Callable callable) { Method callMethod = getCallableMethod(callable.getClass()); for (Class exceptionOrError : callMethod.getExceptionTypes()) { Preconditions.checkArgument(Exception.class.isAssignableFrom(exceptionOrError), - "Callable method exceptions must be dervied from Exception"); - @SuppressWarnings("unchecked") Class exception = - (Class) exceptionOrError; - Preconditions.checkArgument(findMostSpecificRetryInfo(retryInfos, exception) != null, + "Callable method exceptions must be derived from Exception"); + @SuppressWarnings("unchecked") + Class exception = (Class) exceptionOrError; + Preconditions.checkArgument(findMostSpecificRetryInfo(retryInfo, exception) != null, "Declared exception '" + exception + "' is not covered by exception handler"); } } @@ -241,16 +253,16 @@ public Set> getNonRetriableExceptions() { boolean shouldRetry(Exception ex) { for (Interceptor interceptor : interceptors) { - Interceptor.RetryResult retryResult = interceptor.shouldRetry(ex); + Interceptor.RetryResult retryResult = interceptor.beforeEval(ex); if (retryResult != null) { return retryResult.booleanValue(); } } - RetryInfo retryInfo = findMostSpecificRetryInfo(retryInfos, ex.getClass()); + RetryInfo retryInfo = findMostSpecificRetryInfo(this.retryInfo, ex.getClass()); Interceptor.RetryResult retryResult = retryInfo == null ? Interceptor.RetryResult.ABORT : retryInfo.retry; for (Interceptor interceptor : interceptors) { - retryResult = firstNonNull(interceptor.shouldRetry(ex, retryResult), retryResult); + retryResult = firstNonNull(interceptor.afterEval(ex, retryResult), retryResult); } return retryResult.booleanValue(); } diff --git a/src/main/java/com/google/gcloud/RetryHelper.java b/gcloud-java-core/src/main/java/com/google/gcloud/RetryHelper.java similarity index 79% rename from src/main/java/com/google/gcloud/RetryHelper.java rename to gcloud-java-core/src/main/java/com/google/gcloud/RetryHelper.java index ed0fcc1dab85..7b47209cd3ff 100644 --- a/src/main/java/com/google/gcloud/RetryHelper.java +++ b/gcloud-java-core/src/main/java/com/google/gcloud/RetryHelper.java @@ -1,10 +1,26 @@ +/* + * Copyright 2015 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.gcloud; import static com.google.common.base.Preconditions.checkNotNull; -import static java.lang.Math.max; -import static java.lang.Math.min; -import static java.lang.Math.pow; -import static java.lang.Math.random; +import static java.lang.StrictMath.max; +import static java.lang.StrictMath.min; +import static java.lang.StrictMath.pow; +import static java.lang.StrictMath.random; import static java.util.concurrent.TimeUnit.MILLISECONDS; import com.google.common.annotations.VisibleForTesting; @@ -15,6 +31,7 @@ import java.io.InterruptedIOException; import java.nio.channels.ClosedByInterruptException; import java.util.concurrent.Callable; +import java.util.logging.Level; import java.util.logging.Logger; /** @@ -67,7 +84,7 @@ public static final class RetryInterruptedException extends RetryHelperException RetryInterruptedException() {} /** - * Sets the caller thread interrupt flag and throws {@code RetryInteruptedException}. + * Sets the caller thread interrupt flag and throws {@code RetryInterruptedException}. */ public static void propagate() throws RetryInterruptedException { Thread.currentThread().interrupt(); @@ -99,8 +116,8 @@ public static final class NonRetriableException extends RetryHelperException { private static final long serialVersionUID = -2331878521983499652L; - NonRetriableException(Throwable ex) { - super(ex); + NonRetriableException(Throwable throwable) { + super(throwable); } } @@ -122,7 +139,7 @@ public int getAttemptNumber() { } @VisibleForTesting - static final void setContext(Context ctx) { + static void setContext(Context ctx) { if (ctx == null) { context.remove(); } else { @@ -130,7 +147,7 @@ static final void setContext(Context ctx) { } } - static final Context getContext() { + static Context getContext() { return context.get(); } @@ -147,9 +164,11 @@ static final Context getContext() { @Override public String toString() { ToStringHelper toStringHelper = MoreObjects.toStringHelper(this); + toStringHelper.add("params", params); toStringHelper.add("stopwatch", stopwatch); - toStringHelper.add("attempNumber", attemptNumber); + toStringHelper.add("attemptNumber", attemptNumber); toStringHelper.add("callable", callable); + toStringHelper.add("exceptionHandler", exceptionHandler); return toStringHelper.toString(); } @@ -160,16 +179,17 @@ private V doRetry() throws RetryHelperException { Exception exception; try { V value = callable.call(); - if (attemptNumber > 1) { + if (attemptNumber > 1 && log.isLoggable(Level.FINE)) { log.fine(this + ": attempt #" + attemptNumber + " succeeded"); } return value; + } catch (InterruptedException | InterruptedIOException | ClosedByInterruptException e) { + if (!exceptionHandler.shouldRetry(e)) { + RetryInterruptedException.propagate(); + } + exception = e; } catch (Exception e) { if (!exceptionHandler.shouldRetry(e)) { - if (e instanceof InterruptedException || e instanceof InterruptedIOException - || e instanceof ClosedByInterruptException) { - RetryInterruptedException.propagate(); - } throw new NonRetriableException(e); } exception = e; @@ -180,11 +200,14 @@ private V doRetry() throws RetryHelperException { throw new RetriesExhaustedException(this + ": Too many failures, giving up", exception); } long sleepDurationMillis = getSleepDuration(params, attemptNumber); - log.fine(this + ": Attempt #" + attemptNumber + " failed [" + exception + "], sleeping for " - + sleepDurationMillis + " ms"); + if (log.isLoggable(Level.FINE)) { + log.fine(this + ": Attempt #" + attemptNumber + " failed [" + exception + + "], sleeping for " + sleepDurationMillis + " ms"); + } try { Thread.sleep(sleepDurationMillis); } catch (InterruptedException e) { + // propagate as RetryInterruptedException RetryInterruptedException.propagate(); } } diff --git a/src/main/java/com/google/gcloud/RetryParams.java b/gcloud-java-core/src/main/java/com/google/gcloud/RetryParams.java similarity index 91% rename from src/main/java/com/google/gcloud/RetryParams.java rename to gcloud-java-core/src/main/java/com/google/gcloud/RetryParams.java index 4da4c8399c8e..0d38aea77ab0 100644 --- a/src/main/java/com/google/gcloud/RetryParams.java +++ b/gcloud-java-core/src/main/java/com/google/gcloud/RetryParams.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 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.gcloud; import static com.google.common.base.Preconditions.checkArgument; @@ -9,10 +25,10 @@ import java.util.Objects; /** - * Parameters for configuring retries with an exponential backoff. - * Initial request is executed immediately. If the request fails but passes the - * {@link ExceptionHandler} criteria the calling thread sleeps for {@code initialRetryDelayMillis}. - * Each subsequent failure the sleep interval is calculated as: + * Parameters for configuring retries with an exponential backoff. Initial request is executed + * immediately. If the request fails but passes the {@link ExceptionHandler} criteria the calling + * thread sleeps for {@code initialRetryDelayMillis}. Each subsequent failure the sleep interval is + * calculated as: *

* {@code retryDelayBackoffFactor ^ attempts * initialRetryDelayMillis} but would be upper-bounded * to {@code maxRetryDelayMillis} diff --git a/gcloud-java-core/src/main/java/com/google/gcloud/Service.java b/gcloud-java-core/src/main/java/com/google/gcloud/Service.java new file mode 100644 index 000000000000..02d810186bfc --- /dev/null +++ b/gcloud-java-core/src/main/java/com/google/gcloud/Service.java @@ -0,0 +1 @@ +/* * Copyright 2015 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.gcloud; public interface Service { O options(); } \ No newline at end of file diff --git a/gcloud-java-core/src/main/java/com/google/gcloud/ServiceOptions.java b/gcloud-java-core/src/main/java/com/google/gcloud/ServiceOptions.java new file mode 100644 index 000000000000..a974a1f1912a --- /dev/null +++ b/gcloud-java-core/src/main/java/com/google/gcloud/ServiceOptions.java @@ -0,0 +1,325 @@ +/* + * Copyright 2015 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.gcloud; + + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.api.client.extensions.appengine.http.UrlFetchTransport; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.common.collect.Iterables; +import com.google.gcloud.spi.ServiceRpcFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Locale; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class ServiceOptions> implements Serializable { + + private static final String DEFAULT_HOST = "https://www.googleapis.com"; + private static final long serialVersionUID = 1203687993961393350L; + private static final String PROJECT_ENV_NAME = "default_project_id"; + + private final String projectId; + private final String host; + private final HttpTransportFactory httpTransportFactory; + private final AuthCredentials authCredentials; + private final RetryParams retryParams; + private final ServiceRpcFactory serviceRpcFactory; + + public interface HttpTransportFactory extends Serializable { + HttpTransport create(); + } + + private enum DefaultHttpTransportFactory implements HttpTransportFactory { + + INSTANCE; + + @Override + public HttpTransport create() { + // Consider App Engine + if (appEngineAppId() != null) { + try { + return new UrlFetchTransport(); + } catch (Exception ignore) { + // Maybe not on App Engine + } + } + // Consider Compute + try { + return AuthCredentials.getComputeCredential().getTransport(); + } catch (Exception e) { + // Maybe not on GCE + } + return new NetHttpTransport(); + } + } + + + + protected abstract static class Builder, + B extends Builder> { + + private String projectId; + private String host; + private HttpTransportFactory httpTransportFactory; + private AuthCredentials authCredentials; + private RetryParams retryParams; + private ServiceRpcFactory serviceRpcFactory; + + protected Builder() {} + + protected Builder(ServiceOptions options) { + projectId = options.projectId; + host = options.host; + httpTransportFactory = options.httpTransportFactory; + authCredentials = options.authCredentials; + retryParams = options.retryParams; + serviceRpcFactory = options.serviceRpcFactory; + } + + protected abstract ServiceOptions build(); + + @SuppressWarnings("unchecked") + protected B self() { + return (B) this; + } + + public B projectId(String projectId) { + this.projectId = projectId; + return self(); + } + + public B host(String host) { + this.host = host; + return self(); + } + + public B httpTransportFactory(HttpTransportFactory httpTransportFactory) { + this.httpTransportFactory = httpTransportFactory; + return self(); + } + + public B authCredentials(AuthCredentials authCredentials) { + this.authCredentials = authCredentials; + return self(); + } + + public B retryParams(RetryParams retryParams) { + this.retryParams = retryParams; + return self(); + } + + public B serviceRpcFactory(ServiceRpcFactory serviceRpcFactory) { + this.serviceRpcFactory = serviceRpcFactory; + return self(); + } + } + + protected ServiceOptions(Builder builder) { + projectId = checkNotNull(builder.projectId != null ? builder.projectId : defaultProject()); + host = firstNonNull(builder.host, DEFAULT_HOST); + httpTransportFactory = + firstNonNull(builder.httpTransportFactory, DefaultHttpTransportFactory.INSTANCE); + authCredentials = firstNonNull(builder.authCredentials, defaultAuthCredentials()); + retryParams = builder.retryParams; + serviceRpcFactory = builder.serviceRpcFactory; + } + + private static AuthCredentials defaultAuthCredentials() { + // Consider App Engine. This will not be needed once issue #21 is fixed. + if (appEngineAppId() != null) { + try { + return AuthCredentials.createForAppEngine(); + } catch (Exception ignore) { + // Maybe not on App Engine + } + } + + try { + return AuthCredentials.createApplicationDefaults(); + } catch (Exception ex) { + // fallback to old-style + } + + // Consider old-style Compute. This will not be needed once issue #21 is fixed. + try { + return AuthCredentials.createForComputeEngine(); + } catch (Exception ignore) { + // Maybe not on GCE + } + return AuthCredentials.noCredentials(); + } + + protected static String appEngineAppId() { + return System.getProperty("com.google.appengine.application.id"); + } + + protected String defaultProject() { + String projectId = System.getProperty(PROJECT_ENV_NAME, System.getenv(PROJECT_ENV_NAME)); + if (projectId == null) { + projectId = getAppEngineProjectId(); + } + return projectId != null ? projectId : googleCloudProjectId(); + } + + protected static String googleCloudProjectId() { + try { + URL url = new URL("http://metadata/computeMetadata/v1/project/project-id"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestProperty("X-Google-Metadata-Request", "True"); + InputStream input = connection.getInputStream(); + if (connection.getResponseCode() == 200) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, UTF_8))) { + return reader.readLine(); + } + } + } catch (IOException ignore) { + // ignore + } + File configDir; + if (System.getenv().containsKey("CLOUDSDK_CONFIG")) { + configDir = new File(System.getenv("CLOUDSDK_CONFIG")); + } else if (isWindows() && System.getenv().containsKey("APPDATA")) { + configDir = new File(System.getenv("APPDATA"), "gcloud"); + } else { + configDir = new File(System.getProperty("user.home"), ".config/gcloud"); + } + try (BufferedReader reader = + new BufferedReader(new FileReader(new File(configDir, "properties")))) { + String line; + String section = null; + Pattern projectPattern = Pattern.compile("^project\\s*=\\s*(.*)$"); + Pattern sectionPattern = Pattern.compile("^\\[(.*)\\]$"); + while((line = reader.readLine()) != null) { + if (line.isEmpty() || line.startsWith(";")) { + continue; + } + line = line.trim(); + Matcher matcher = sectionPattern.matcher(line); + if (matcher.matches()) { + section = matcher.group(1); + } else if (section == null || section.equals("core")) { + matcher = projectPattern.matcher(line); + if (matcher.matches()) { + return matcher.group(1); + } + } + } + } catch (IOException ex) { + // ignore + } + // return null if can't determine + return null; + } + + private static boolean isWindows() { + return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).indexOf("windows") > -1; + } + + protected static String getAppEngineProjectId() { + // TODO(ozarov): An alternative to reflection would be to depend on AE api jar: + // http://mvnrepository.com/artifact/com.google.appengine/appengine-api-1.0-sdk/1.2.0 + try { + Class factoryClass = + Class.forName("com.google.appengine.api.appidentity.AppIdentityServiceFactory"); + Method method = factoryClass.getMethod("getAppIdentityService"); + Object appIdentityService = method.invoke(null); + method = appIdentityService.getClass().getMethod("getServiceAccountName"); + String serviceAccountName = (String) method.invoke(appIdentityService); + int indexOfAtSign = serviceAccountName.indexOf('@'); + return serviceAccountName.substring(0, indexOfAtSign); + } catch (Exception ignore) { + // return null if can't determine + return null; + } + } + + protected abstract Set scopes(); + + public String projectId() { + return projectId; + } + + public String host() { + return host; + } + + public HttpTransportFactory httpTransportFactory() { + return httpTransportFactory; + } + + public AuthCredentials authCredentials() { + return authCredentials; + } + + public RetryParams retryParams() { + return retryParams != null ? retryParams : RetryParams.noRetries(); + } + + public ServiceRpcFactory serviceRpcFactory() { + return serviceRpcFactory; + } + + public HttpRequestInitializer httpRequestInitializer() { + HttpTransport httpTransport = httpTransportFactory.create(); + return authCredentials().httpRequestInitializer(httpTransport, scopes()); + } + + @Override + public int hashCode() { + return Objects.hash(projectId, host, httpTransportFactory, authCredentials, retryParams, + serviceRpcFactory); + } + + protected boolean isEquals(ServiceOptions other) { + return Objects.equals(projectId, other.projectId) + && Objects.equals(host, other.host) + && Objects.equals(httpTransportFactory, other.httpTransportFactory) + && Objects.equals(authCredentials, other.authCredentials) + && Objects.equals(retryParams, other.retryParams) + && Objects.equals(serviceRpcFactory, other.serviceRpcFactory); + } + + public abstract Builder toBuilder(); + + /** + * Creates a service RPC using a factory loaded by {@link ServiceLoader}. + */ + protected static > R createRpc(O options, + Class> factoryClass) { + ServiceRpcFactory factory = Iterables.getFirst(ServiceLoader.load(factoryClass), null); + return factory == null ? null : factory.create(options); + } +} diff --git a/gcloud-java-core/src/main/java/com/google/gcloud/spi/ServiceRpcFactory.java b/gcloud-java-core/src/main/java/com/google/gcloud/spi/ServiceRpcFactory.java new file mode 100644 index 000000000000..e8e67615305d --- /dev/null +++ b/gcloud-java-core/src/main/java/com/google/gcloud/spi/ServiceRpcFactory.java @@ -0,0 +1 @@ +/* * Copyright 2015 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.gcloud.spi; import com.google.gcloud.ServiceOptions; import java.io.Serializable; /** * A base interface for all service RPC factories. * Loading of a factory implementation is done via {@link java.util.ServiceLoader}. */ public interface ServiceRpcFactory extends Serializable { S create(O options); } \ No newline at end of file diff --git a/src/test/java/com/google/gcloud/ExceptionHandlerTest.java b/gcloud-java-core/src/test/java/com/google/gcloud/ExceptionHandlerTest.java similarity index 84% rename from src/test/java/com/google/gcloud/ExceptionHandlerTest.java rename to gcloud-java-core/src/test/java/com/google/gcloud/ExceptionHandlerTest.java index 35f9613c42ae..3844f9de36d7 100644 --- a/src/test/java/com/google/gcloud/ExceptionHandlerTest.java +++ b/gcloud-java-core/src/test/java/com/google/gcloud/ExceptionHandlerTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 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.gcloud; import static org.junit.Assert.assertFalse; @@ -35,28 +51,28 @@ class B extends A { class C extends A { @Override public Object call() throws FileNotFoundException { - return null; + return "c"; } } class D extends C { @Override public Object call() throws IllegalArgumentException { - return null; + return "d"; } } class E extends A { @Override public String call() throws NullPointerException { - return null; + return "e"; } } class F extends A { @Override public Object call() throws Error { - return null; + return "f"; } } @@ -93,7 +109,6 @@ private static void assertInvalidCallable(Callable callable, ExceptionHan } } - @SuppressWarnings("serial") @Test public void testShouldTry() { ExceptionHandler handler = ExceptionHandler.builder().retryOn(IOException.class).build(); @@ -114,14 +129,16 @@ public void testShouldTry() { assertTrue(handler.shouldRetry(new NullPointerException())); final AtomicReference before = new AtomicReference<>(RetryResult.ABORT); + @SuppressWarnings("serial") Interceptor interceptor = new Interceptor() { + @Override - public RetryResult shouldRetry(Exception exception, RetryResult retryResult) { + public RetryResult afterEval(Exception exception, RetryResult retryResult) { return retryResult == RetryResult.ABORT ? RetryResult.RETRY : RetryResult.ABORT; } @Override - public RetryResult shouldRetry(Exception exception) { + public RetryResult beforeEval(Exception exception) { return before.get(); } }; diff --git a/src/test/java/com/google/gcloud/RetryHelperTest.java b/gcloud-java-core/src/test/java/com/google/gcloud/RetryHelperTest.java similarity index 77% rename from src/test/java/com/google/gcloud/RetryHelperTest.java rename to gcloud-java-core/src/test/java/com/google/gcloud/RetryHelperTest.java index 257dc892d229..dfd933bcae46 100644 --- a/src/test/java/com/google/gcloud/RetryHelperTest.java +++ b/gcloud-java-core/src/test/java/com/google/gcloud/RetryHelperTest.java @@ -1,11 +1,11 @@ /* - * Copyright 2012 Google Inc. All Rights Reserved. + * Copyright 2015 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 + * 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, @@ -42,6 +42,22 @@ */ public class RetryHelperTest { + static class E1Exception extends Exception { + private static final long serialVersionUID = 3874933713392137001L; + } + + static class E2Exception extends E1Exception { + private static final long serialVersionUID = -8710227162480133598L; + } + + static class E3Exception extends E1Exception { + private static final long serialVersionUID = -7794256022024001666L; + } + + static class E4Exception extends E2Exception { + private static final long serialVersionUID = -5508018234693709156L; + } + @Test public void testTriesWithExceptionHandling() { assertNull(RetryHelper.getContext()); @@ -68,32 +84,21 @@ public void testTriesWithExceptionHandling() { } assertNull(RetryHelper.getContext()); - @SuppressWarnings("serial") - class E1 extends Exception {} - - @SuppressWarnings("serial") - class E2 extends E1 {} - - @SuppressWarnings("serial") - class E3 extends E1 {} - - @SuppressWarnings("serial") - class E4 extends E2 {} - params = RetryParams.builder().initialRetryDelayMillis(0).retryMaxAttempts(5).build(); - handler = ExceptionHandler.builder().retryOn(E1.class, E4.class).abortOn(E3.class).build(); - final Iterator exceptions = - Arrays.asList(new E1(), new E2(), new E4(), new E3()).iterator(); + handler = ExceptionHandler.builder() + .retryOn(E1Exception.class, E4Exception.class) + .abortOn(E3Exception.class).build(); + final Iterator exceptions = Arrays.asList( + new E1Exception(), new E2Exception(), new E4Exception(), new E3Exception()).iterator(); try { RetryHelper.runWithRetries(new Callable() { - @Override public Void call() throws E1 { - E1 exception = exceptions.next(); - throw exception; + @Override public Void call() throws E1Exception { + throw exceptions.next(); } }, params, handler); fail("Exception should have been thrown"); } catch (NonRetriableException ex) { - assertTrue(ex.getCause() instanceof E3); + assertTrue(ex.getCause() instanceof E3Exception); } assertNull(RetryHelper.getContext()); } @@ -109,7 +114,7 @@ public void testTriesAtLeastMinTimes() { final int timesToFail = 7; assertNull(RetryHelper.getContext()); int attempted = RetryHelper.runWithRetries(new Callable() { - int timesCalled = 0; + int timesCalled; @Override public Integer call() throws IOException { timesCalled++; assertEquals(timesCalled, RetryHelper.getContext().getAttemptNumber()); @@ -152,18 +157,12 @@ public void testTriesNoMoreThanMaxTimes() { } } - private class FakeTicker extends Ticker { + private static class FakeTicker extends Ticker { private final AtomicLong nanos = new AtomicLong(); // Advances the ticker value by {@code time} in {@code timeUnit}. - FakeTicker advance(long time, TimeUnit timeUnit) { - return advance(timeUnit.toNanos(time)); - } - - // Advances the ticker value by {@code nanoseconds}. - FakeTicker advance(long nanoseconds) { - nanos.addAndGet(nanoseconds); - return this; + void advance(long time, TimeUnit timeUnit) { + nanos.addAndGet(timeUnit.toNanos(time)); } @Override @@ -197,7 +196,8 @@ public void testTriesNoMoreLongerThanTotalRetryPeriod() { } }), params, handler, stopwatch); fail(); - } catch (RetriesExhaustedException e) { + } catch (RetriesExhaustedException expected) { + // verify timesCalled assertEquals(sleepOnAttempt, timesCalled.get()); } } @@ -214,29 +214,29 @@ public void testBackoffIsExponential() { .retryMaxAttempts(100) .build(); long sleepDuration = RetryHelper.getSleepDuration(params, 1); - assertTrue("" + sleepDuration, sleepDuration < 13 && sleepDuration >= 7); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 13 && sleepDuration >= 7); sleepDuration = RetryHelper.getSleepDuration(params, 2); - assertTrue("" + sleepDuration, sleepDuration < 25 && sleepDuration >= 15); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 25 && sleepDuration >= 15); sleepDuration = RetryHelper.getSleepDuration(params, 3); - assertTrue("" + sleepDuration, sleepDuration < 50 && sleepDuration >= 30); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 50 && sleepDuration >= 30); sleepDuration = RetryHelper.getSleepDuration(params, 4); - assertTrue("" + sleepDuration, sleepDuration < 100 && sleepDuration >= 60); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 100 && sleepDuration >= 60); sleepDuration = RetryHelper.getSleepDuration(params, 5); - assertTrue("" + sleepDuration, sleepDuration < 200 && sleepDuration >= 120); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 200 && sleepDuration >= 120); sleepDuration = RetryHelper.getSleepDuration(params, 6); - assertTrue("" + sleepDuration, sleepDuration < 400 && sleepDuration >= 240); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 400 && sleepDuration >= 240); sleepDuration = RetryHelper.getSleepDuration(params, 7); - assertTrue("" + sleepDuration, sleepDuration < 800 && sleepDuration >= 480); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 800 && sleepDuration >= 480); sleepDuration = RetryHelper.getSleepDuration(params, 8); - assertTrue("" + sleepDuration, sleepDuration < 1600 && sleepDuration >= 960); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 1600 && sleepDuration >= 960); sleepDuration = RetryHelper.getSleepDuration(params, 9); - assertTrue("" + sleepDuration, sleepDuration < 3200 && sleepDuration >= 1920); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 3200 && sleepDuration >= 1920); sleepDuration = RetryHelper.getSleepDuration(params, 10); - assertTrue("" + sleepDuration, sleepDuration < 6400 && sleepDuration >= 3840); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 6400 && sleepDuration >= 3840); sleepDuration = RetryHelper.getSleepDuration(params, 11); - assertTrue("" + sleepDuration, sleepDuration < 12800 && sleepDuration >= 7680); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 12800 && sleepDuration >= 7680); sleepDuration = RetryHelper.getSleepDuration(params, 12); - assertTrue("" + sleepDuration, sleepDuration < 25600 && sleepDuration >= 15360); + assertTrue(String.valueOf(sleepDuration), sleepDuration < 25600 && sleepDuration >= 15360); } @Test diff --git a/src/test/java/com/google/gcloud/RetryParamsTest.java b/gcloud-java-core/src/test/java/com/google/gcloud/RetryParamsTest.java similarity index 78% rename from src/test/java/com/google/gcloud/RetryParamsTest.java rename to gcloud-java-core/src/test/java/com/google/gcloud/RetryParamsTest.java index b59324f0cb1e..d1d5e3c076d8 100644 --- a/src/test/java/com/google/gcloud/RetryParamsTest.java +++ b/gcloud-java-core/src/test/java/com/google/gcloud/RetryParamsTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 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.gcloud; import static com.google.gcloud.RetryParams.DEFAULT_INITIAL_RETRY_DELAY_MILLIS; @@ -62,24 +78,24 @@ public void testSetAndCopy() { public void testBadSettings() { RetryParams.Builder builder = RetryParams.builder(); builder.initialRetryDelayMillis(-1); - builder = verifyFailure(builder); + builder = assertFailure(builder); builder.maxRetryDelayMillis(RetryParams.getDefaultInstance().getInitialRetryDelayMillis() - 1); - builder = verifyFailure(builder); + builder = assertFailure(builder); builder.retryDelayBackoffFactor(-1); - builder = verifyFailure(builder); + builder = assertFailure(builder); builder.retryMinAttempts(-1); - builder = verifyFailure(builder); + builder = assertFailure(builder); builder.retryMaxAttempts(RetryParams.getDefaultInstance().getRetryMinAttempts() - 1); - builder = verifyFailure(builder); + builder = assertFailure(builder); builder.totalRetryPeriodMillis(-1); - builder = verifyFailure(builder); + builder = assertFailure(builder); // verify that it is OK for min and max to be equal builder.retryMaxAttempts(RetryParams.getDefaultInstance().getRetryMinAttempts()); builder.maxRetryDelayMillis(RetryParams.getDefaultInstance().getInitialRetryDelayMillis()); builder.build(); } - private static Builder verifyFailure(Builder builder) { + private static Builder assertFailure(Builder builder) { try { builder.build(); fail("Expected IllegalArgumentException"); diff --git a/gcloud-java-datastore/README.md b/gcloud-java-datastore/README.md new file mode 100644 index 000000000000..7113dbdd0231 --- /dev/null +++ b/gcloud-java-datastore/README.md @@ -0,0 +1,106 @@ +Google Cloud Java Client +========================== + +Java idiomatic client for [Google Cloud Platform][cloud-platform] services. + +[![Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-java.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/gcloud-java) +[![Coverage Status](https://coveralls.io/repos/GoogleCloudPlatform/gcloud-java/badge.svg?branch=master)](https://coveralls.io/r/GoogleCloudPlatform/gcloud-java?branch=master) + +- [Homepage] (https://googlecloudplatform.github.io/gcloud-java/) +- [API Documentation] (http://googlecloudplatform.github.io/gcloud-java/apidocs) +- [Examples] (http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/examples/package-summary.html) + +This client supports [Google Cloud Datastore] (https://cloud.google.com/datastore/) + + +> Note: This client is a work-in-progress, and may occasionally +> make backwards-incompatible changes. + +Quickstart +---------- +Add this to your pom.xml file +```xml + + com.google.gcloud + gcloud-java-datastore + LATEST + +``` + +Google [Cloud Datastore][cloud-datastore] is a fully managed, schemaless database for +storing non-relational data. Cloud Datastore automatically scales with +your users and supports ACID transactions, high availability of reads and +writes, strong consistency for reads and ancestor queries, and eventual +consistency for all other queries. + +See the [Google Cloud Datastore docs][cloud-datastore-activation] for more details on how to activate +Cloud Datastore for your project. + +See the ``gcloud-java`` API [datastore documentation][datastore-api] to learn how to interact +with the Cloud Datastore using this Client Library. + +```java +import com.google.gcloud.datastore.Datastore; +import com.google.gcloud.datastore.DatastoreFactory; +import com.google.gcloud.datastore.DatastoreOptions; +import com.google.gcloud.datastore.DateTime; +import com.google.gcloud.datastore.Entity; +import com.google.gcloud.datastore.Key; +import com.google.gcloud.datastore.KeyFactory; + +DatastoreOptions options = DatastoreOptions.builder().projectId(PROJECT_ID).build(); +Datastore datastore = DatastoreFactory.instance().get(options); +KeyFactory keyFactory = datastore.newKeyFactory().kind(KIND); +Key key = keyFactory.newKey(keyName); +Entity entity = datastore.get(key); +if (entity == null) { + entity = Entity.builder(key) + .set("name", "John Do") + .set("age", 30) + .set("access_time", DateTime.now()) + .build(); + datastore.put(entity); +} else { + System.out.println("Updating access_time for " + entity.getString("name")); + entity = Entity.builder(entity) + .set("access_time", DateTime.now()) + .build(); + datastore.update(entity); +} +``` + +Contributing +------------ + +Contributions to this library are always welcome and highly encouraged. + +See [CONTRIBUTING] for more information on how to get started. + +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. + +License +------- + +Apache 2.0 - See [LICENSE] for more information. + + +[CONTRIBUTING]:https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/CONTRIBUTING.md +[LICENSE]: https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/LICENSE +[cloud-platform]: https://cloud.google.com/ +[cloud-datastore]: https://cloud.google.com/datastore/docs +[cloud-datastore-docs]: https://cloud.google.com/datastore/docs +[cloud-datastore-activation]: https://cloud.google.com/datastore/docs/activate +[datastore-api]: http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/datastore/package-summary.html + diff --git a/gcloud-java-datastore/pom.xml b/gcloud-java-datastore/pom.xml new file mode 100644 index 000000000000..b890e6d2b755 --- /dev/null +++ b/gcloud-java-datastore/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + com.google.gcloud + gcloud-java-datastore + jar + GCloud Java datastore + https://github.com/GoogleCloudPlatform/gcloud-java + + Java idiomatic client for Google Cloud Datastore. + + + com.google.gcloud + gcloud-java-pom + 0.0.5 + + + + ${project.groupId} + gcloud-java-core + ${project.version} + + + com.google.apis + google-api-services-datastore-protobuf + v1beta2-rev1-2.1.2 + compile + + + com.google.apis + google-api-services-datastore + v1beta2-rev23-1.19.0 + + + junit + junit + 4.12 + test + + + org.easymock + easymock + 3.3 + test + + + diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BaseDatastoreBatchWriter.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BaseDatastoreBatchWriter.java new file mode 100644 index 000000000000..7eaf5c535f26 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BaseDatastoreBatchWriter.java @@ -0,0 +1,223 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import com.google.api.services.datastore.DatastoreV1; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Base class for DatastoreBatchWriter. + */ +public abstract class BaseDatastoreBatchWriter implements DatastoreBatchWriter { + + private final String name; + private final Map> toAdd = new LinkedHashMap<>(); + private final List> toAddAutoId = new LinkedList<>(); + private final Map> toUpdate = new LinkedHashMap<>(); + private final Map> toPut = new LinkedHashMap<>(); + private final Set toDelete = new LinkedHashSet<>(); + private boolean active = true; + + protected BaseDatastoreBatchWriter(String name) { + this.name = name; + } + + @SuppressWarnings("unchecked") + @Override + public final void addWithDeferredIdAllocation(FullEntity... entities) { + validateActive(); + for (FullEntity entity : entities) { + IncompleteKey key = entity.key(); + Preconditions.checkArgument(key != null, "Entity must have a key"); + if (key instanceof Key) { + addInternal((FullEntity) entity); + } else { + toAddAutoId.add((FullEntity) entity); + } + } + } + + private void addInternal(FullEntity entity) { + Key key = entity.key(); + if (toAdd.containsKey(key) || toUpdate.containsKey(key) || toPut.containsKey(key)) { + throw newInvalidRequest("Entity with the key %s was already added or updated in this %s", + entity.key(), name); + } + if (toDelete.remove(key)) { + toPut.put(key, entity); + } else { + toAdd.put(key, entity); + } + } + + @Override + public final Entity add(FullEntity entity) { + return DatastoreHelper.add(this, entity); + } + + @SuppressWarnings("unchecked") + @Override + public final List add(FullEntity... entities) { + validateActive(); + List incompleteKeys = Lists.newArrayListWithExpectedSize(entities.length); + for (FullEntity entity : entities) { + IncompleteKey key = entity.key(); + Preconditions.checkArgument(key != null, "Entity must have a key"); + if (key instanceof Key) { + addInternal((FullEntity) entity); + } else { + incompleteKeys.add(key); + } + } + Iterator allocated; + if (!incompleteKeys.isEmpty()) { + IncompleteKey[] toAllocate = Iterables.toArray(incompleteKeys, IncompleteKey.class); + allocated = datastore().allocateId(toAllocate).iterator(); + } else { + allocated = Collections.emptyIterator(); + } + List answer = Lists.newArrayListWithExpectedSize(entities.length); + for (FullEntity entity : entities) { + if (entity.key() instanceof Key) { + answer.add(Entity.convert((FullEntity) entity)); + } else { + Entity entityWithAllocatedId = Entity.builder(allocated.next(), entity).build(); + addInternal(entityWithAllocatedId); + answer.add(entityWithAllocatedId); + } + } + return answer; + } + + @SafeVarargs + @Override + public final void update(Entity... entities) { + validateActive(); + for (Entity entity : entities) { + Key key = entity.key(); + if (toDelete.contains(key)) { + throw newInvalidRequest("Entity with the key %s was already deleted in this %s", + entity.key(), name); + } + if (toAdd.remove(key) != null || toPut.containsKey(key)) { + toPut.put(key, entity); + } else { + toUpdate.put(key, entity); + } + } + } + + @SafeVarargs + @Override + public final void put(Entity... entities) { + validateActive(); + for (Entity entity : entities) { + Key key = entity.key(); + toAdd.remove(key); + toUpdate.remove(key); + toDelete.remove(key); + toPut.put(key, entity); + } + } + + @Override + public final void delete(Key... keys) { + validateActive(); + for (Key key : keys) { + toAdd.remove(key); + toUpdate.remove(key); + toPut.remove(key); + toDelete.add(key); + } + } + + @Override + public boolean active() { + return active; + } + + protected String name() { + return name; + } + + protected Map> toAdd() { + return toAdd; + } + + protected List> toAddAutoId() { + return toAddAutoId; + } + + protected Map> toUpdate() { + return toUpdate; + } + + protected Map> toPut() { + return toPut; + } + + protected Set toDelete() { + return toDelete; + } + + protected void deactivate() { + active = false; + } + + protected void validateActive() { + if (!active) { + throw newInvalidRequest("%s is no longer active", name); + } + } + + protected DatastoreException newInvalidRequest(String msg, Object... params) { + return DatastoreException.throwInvalidRequest(String.format(msg, params)); + } + + protected DatastoreV1.Mutation.Builder toMutationPb() { + DatastoreV1.Mutation.Builder mutationPb = DatastoreV1.Mutation.newBuilder(); + for (FullEntity entity : toAddAutoId()) { + mutationPb.addInsertAutoId(entity.toPb()); + } + for (FullEntity entity : toAdd().values()) { + mutationPb.addInsert(entity.toPb()); + } + for (FullEntity entity : toUpdate().values()) { + mutationPb.addUpdate(entity.toPb()); + } + for (FullEntity entity : toPut().values()) { + mutationPb.addUpsert(entity.toPb()); + } + for (Key key : toDelete()) { + mutationPb.addDelete(key.toPb()); + } + return mutationPb; + } + + protected abstract Datastore datastore(); +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BaseEntity.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BaseEntity.java new file mode 100644 index 000000000000..21e20b33a8ed --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BaseEntity.java @@ -0,0 +1,399 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import static com.google.gcloud.datastore.BlobValue.of; +import static com.google.gcloud.datastore.BooleanValue.of; +import static com.google.gcloud.datastore.DateTimeValue.of; +import static com.google.gcloud.datastore.DoubleValue.of; +import static com.google.gcloud.datastore.EntityValue.of; +import static com.google.gcloud.datastore.KeyValue.of; +import static com.google.gcloud.datastore.ListValue.of; +import static com.google.gcloud.datastore.LongValue.of; +import static com.google.gcloud.datastore.NullValue.of; +import static com.google.gcloud.datastore.StringValue.of; + +import com.google.api.services.datastore.DatastoreV1; +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Maps; +import com.google.protobuf.InvalidProtocolBufferException; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * A base class for entities (key and properties). + * An entity is Google Cloud Datastore persistent data object. + * An entity holds one or more properties, represented by a name (as {@link String}) + * and a value (as {@link com.google.gcloud.datastore.Value}), and may be associated with a + * key. For a list of possible values see {@link ValueType}. + * + * @see Google Cloud Datastore Entities, Properties, and Keys + */ +public abstract class BaseEntity extends Serializable { + + private static final long serialVersionUID = 8175618724683792766L; + + private final transient ImmutableSortedMap> properties; + private final K key; + + abstract static class Builder> { + + private K key; + private final Map> properties = new HashMap<>(); + + Builder() { + } + + Builder(K key) { + key(key); + } + + Builder(BaseEntity entity) { + this(entity.key, entity); + } + + Builder(K key, BaseEntity entity) { + key(key); + properties(entity.properties); + } + + protected K key() { + return key; + } + + protected Map> properties() { + return properties; + } + + @SuppressWarnings("unchecked") + private B self() { + return (B) this; + } + + @SuppressWarnings("unchecked") + protected B fill(DatastoreV1.Entity entityPb) { + Map> copiedProperties = Maps.newHashMap(); + for (DatastoreV1.Property property : entityPb.getPropertyList()) { + copiedProperties.put(property.getName(), Value.fromPb(property.getValue())); + } + properties(copiedProperties); + if (entityPb.hasKey()) { + key((K) IncompleteKey.fromPb(entityPb.getKey())); + } + return self(); + } + + protected B properties(Map> properties) { + this.properties.putAll(properties); + return self(); + } + + public B key(K key) { + this.key = key; + return self(); + } + + /** + * Clears all the properties. + */ + public B clear() { + properties.clear(); + return self(); + } + + /** + * Removes a property with the given {@code name}. + */ + public B remove(String name) { + properties.remove(name); + return self(); + } + + public B set(String name, Value value) { + properties.put(name, value); + return self(); + } + + public B set(String name, String value) { + properties.put(name, of(value)); + return self(); + } + + public B set(String name, long value) { + properties.put(name, of(value)); + return self(); + } + + public B set(String name, double value) { + properties.put(name, of(value)); + return self(); + } + + public B set(String name, boolean value) { + properties.put(name, of(value)); + return self(); + } + + public B set(String name, DateTime value) { + properties.put(name, of(value)); + return self(); + } + + public B set(String name, Key value) { + properties.put(name, of(value)); + return self(); + } + + public B set(String name, FullEntity value) { + properties.put(name, of(value)); + return self(); + } + + public B set(String name, List> values) { + properties.put(name, of(values)); + return self(); + } + + public B set(String name, Value value, Value... other) { + properties.put(name, of(value, other)); + return self(); + } + + public B set(String name, Blob value) { + properties.put(name, of(value)); + return self(); + } + + public B setNull(String name) { + properties.put(name, of()); + return self(); + } + + public abstract BaseEntity build(); + } + + BaseEntity(Builder builder) { + this.key = builder.key; + this.properties = ImmutableSortedMap.copyOf(builder.properties); + } + + BaseEntity(BaseEntity from) { + this.key = from.key(); + this.properties = from.properties; + } + + @Override + public int hashCode() { + return Objects.hash(key, properties); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof BaseEntity)) { + return false; + } + BaseEntity other = (BaseEntity) obj; + return Objects.equals(key, other.key) + && Objects.equals(properties, other.properties); + } + + /** + * Returns true if entity has a non-null key. + */ + public boolean hasKey() { + return key != null; + } + + /** + * Returns the associated key or null if it does not have one. + */ + public K key() { + return key; + } + + /** + * Returns {@code true} if the entity contains a property with the given {@code name}. + */ + public boolean contains(String name) { + return properties.containsKey(name); + } + + /** + * Returns the {@link Value} for the given property {@code name}. + * + * @throws DatastoreException if not such property. + */ + public > V getValue(String name) { + @SuppressWarnings("unchecked") + V property = (V) properties.get(name); + if (property == null) { + throw DatastoreException.throwInvalidRequest("No such property %s", name); + } + return property; + } + + /** + * Returns true if property is instanceof NullValue. + * + * @throws DatastoreException if not such property. + */ + public boolean isNull(String name) { + return getValue(name) instanceof NullValue; + } + + + /** + * Returns the property value as a string. + * + * @throws DatastoreException if not such property. + * @throws ClassCastException if value is not a string. + */ + @SuppressWarnings("unchecked") + public String getString(String name) { + return ((Value) getValue(name)).get(); + } + + /** + * Returns the property value as long. + * + * @throws DatastoreException if not such property. + * @throws ClassCastException if value is not a long. + */ + @SuppressWarnings("unchecked") + public long getLong(String name) { + return ((Value) getValue(name)).get(); + } + + /** + * Returns the property value as a double. + * + * @throws DatastoreException if not such property. + * @throws ClassCastException if value is not a double. + */ + @SuppressWarnings("unchecked") + public double getDouble(String name) { + return ((Value) getValue(name)).get(); + } + + /** + * Returns the property value as a boolean. + * + * @throws DatastoreException if not such property. + * @throws ClassCastException if value is not a boolean. + */ + @SuppressWarnings("unchecked") + public boolean getBoolean(String name) { + return ((Value) getValue(name)).get(); + } + + /** + * Returns the property value as a DateTime. + * + * @throws DatastoreException if not such property. + * @throws ClassCastException if value is not a DateTime. + */ + @SuppressWarnings("unchecked") + public DateTime getDateTime(String name) { + return ((Value) getValue(name)).get(); + } + + /** + * Returns the property value as a Key. + * + * @throws DatastoreException if not such property. + * @throws ClassCastException if value is not a Key. + */ + @SuppressWarnings("unchecked") + public Key getKey(String name) { + return ((Value) getValue(name)).get(); + } + + /** + * Returns the property value as an entity. + * + * @throws DatastoreException if not such property. + * @throws ClassCastException if value is not an entity. + */ + @SuppressWarnings("unchecked") + public FullEntity getEntity(String name) { + return ((Value>) getValue(name)).get(); + } + + /** + * Returns the property value as a list of values. + * + * @throws DatastoreException if not such property. + * @throws ClassCastException if value is not a list of values. + */ + @SuppressWarnings("unchecked") + public List> getList(String name) { + return ((Value>>) getValue(name)).get(); + } + + /** + * Returns the property value as a blob. + * + * @throws DatastoreException if not such property. + * @throws ClassCastException if value is not a blob. + */ + @SuppressWarnings("unchecked") + public Blob getBlob(String name) { + return ((Value) getValue(name)).get(); + } + + /** + * Returns the properties name. + */ + public Set names() { + return properties.keySet(); + } + + ImmutableSortedMap> properties() { + return properties; + } + + @Override + protected Object fromPb(byte[] bytesPb) throws InvalidProtocolBufferException { + Builder builder = emptyBuilder(); + builder.fill(DatastoreV1.Entity.parseFrom(bytesPb)); + return builder.build(); + } + + protected abstract Builder emptyBuilder(); + + @Override + protected final DatastoreV1.Entity toPb() { + DatastoreV1.Entity.Builder entityPb = DatastoreV1.Entity.newBuilder(); + for (Map.Entry> entry : properties.entrySet()) { + DatastoreV1.Property.Builder propertyPb = DatastoreV1.Property.newBuilder(); + propertyPb.setName(entry.getKey()); + propertyPb.setValue(entry.getValue().toPb()); + entityPb.addProperty(propertyPb.build()); + } + if (key != null) { + entityPb.setKey(key.toPb()); + } + return entityPb.build(); + } +} diff --git a/src/main/java/com/google/gcloud/datastore/BaseKey.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BaseKey.java similarity index 68% rename from src/main/java/com/google/gcloud/datastore/BaseKey.java rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BaseKey.java index ea9f600aff7e..865b95ed8518 100644 --- a/src/main/java/com/google/gcloud/datastore/BaseKey.java +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BaseKey.java @@ -1,6 +1,22 @@ +/* + * Copyright 2015 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.gcloud.datastore; -import static com.google.gcloud.datastore.Validator.validateDataset; +import static com.google.gcloud.datastore.Validator.validateDatabase; import static com.google.gcloud.datastore.Validator.validateKind; import static com.google.gcloud.datastore.Validator.validateNamespace; @@ -19,38 +35,38 @@ abstract class BaseKey extends Serializable { private static final long serialVersionUID = -4671243265877410635L; - private final transient String dataset; + private final transient String projectId; private final transient String namespace; private final transient ImmutableList path; abstract static class Builder> { - protected String dataset; - protected String namespace; - protected String kind; - protected final List ancestors; + String projectId; + String namespace; + String kind; + final List ancestors; private static final int MAX_PATH = 100; - public Builder(String dataset) { - this.dataset = validateDataset(dataset); + Builder(String projectId) { + this.projectId = validateDatabase(projectId); ancestors = new LinkedList<>(); } - public Builder(String dataset, String kind) { - this(dataset); + Builder(String projectId, String kind) { + this(projectId); this.kind = validateKind(kind); } - public Builder(BaseKey copyFrom) { - dataset = copyFrom.dataset(); + Builder(BaseKey copyFrom) { + projectId = copyFrom.projectId(); namespace = copyFrom.namespace(); ancestors = new LinkedList<>(copyFrom.ancestors()); kind = copyFrom.kind(); } @SuppressWarnings("unchecked") - protected B self() { + B self() { return (B) this; } @@ -77,8 +93,8 @@ public B kind(String kind) { return self(); } - public B dataset(String dataset) { - this.dataset = validateDataset(dataset); + public B projectId(String projectId) { + this.projectId = validateDatabase(projectId); return self(); } @@ -90,17 +106,18 @@ public B namespace(String namespace) { protected abstract BaseKey build(); } - BaseKey(String dataset, String namespace, ImmutableList path) { - this.dataset = dataset; + BaseKey(String projectId, String namespace, ImmutableList path) { + Preconditions.checkArgument(!path.isEmpty(), "Path must not be empty"); + this.projectId = projectId; this.namespace = namespace; this.path = path; } /** - * Returns the key's dataset. + * Returns the key's projectId. */ - public String dataset() { - return dataset; + public String projectId() { + return projectId; } /** @@ -120,11 +137,11 @@ public List ancestors() { /** * Returns an immutable list of the key's path (ancestors + self). */ - public List path() { + List path() { return path; } - protected PathElement leaf() { + PathElement leaf() { return path().get(path().size() - 1); } @@ -137,7 +154,7 @@ public String kind() { @Override public int hashCode() { - return Objects.hash(dataset(), namespace(), path()); + return Objects.hash(projectId(), namespace(), path()); } @Override @@ -148,8 +165,8 @@ public boolean equals(Object obj) { if (!(obj instanceof BaseKey)) { return false; } - PartialKey other = (PartialKey) obj; - return Objects.equals(dataset(), other.dataset()) + BaseKey other = (BaseKey) obj; + return Objects.equals(projectId(), other.projectId()) && Objects.equals(namespace(), other.namespace()) && Objects.equals(path(), other.path()); } @@ -158,8 +175,8 @@ public boolean equals(Object obj) { protected DatastoreV1.Key toPb() { DatastoreV1.Key.Builder keyPb = DatastoreV1.Key.newBuilder(); DatastoreV1.PartitionId.Builder partitionIdPb = DatastoreV1.PartitionId.newBuilder(); - if (dataset != null) { - partitionIdPb.setDatasetId(dataset); + if (projectId != null) { + partitionIdPb.setDatasetId(projectId); } if (namespace != null) { partitionIdPb.setNamespace(namespace); diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Batch.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Batch.java new file mode 100644 index 000000000000..75a5d1381403 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Batch.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import java.util.List; + +/** + * An interface to represent a batch of write operations. + * Any write operation that is applied on a batch will only be sent + * to the Datastore upon {@link #submit}. + * A usage example: + *

 {@code
+ *   Entity entity1 = datastore.get(key1);
+ *   Batch batch = datastore.newBatch();
+ *   Entity entity2 = Entity.builder(key2).set("name", "John").build();
+ *   entity1 = Entity.builder(entity1).clear().setNull("bla").build();
+ *   Entity entity3 = Entity.builder(key3).set("title", "title").build();
+ *   batch.update(entity1);
+ *   batch.add(entity2, entity3);
+ *   batch.submit();
+ * } 
+ */ +public interface Batch extends DatastoreBatchWriter { + + interface Response { + List generatedKeys(); + } + + /** + * Submit the batch to the Datastore. + * + * @throws DatastoreException if there was any failure or if batch is not longer active + */ + Response submit(); + + /** + * Returns the batch associated {@link Datastore}. + */ + Datastore datastore(); +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BatchImpl.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BatchImpl.java new file mode 100644 index 000000000000..9c95949e2c8a --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BatchImpl.java @@ -0,0 +1,82 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import com.google.api.services.datastore.DatastoreV1; +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import com.google.gcloud.datastore.BatchOption.ForceWrites; + +import java.util.List; +import java.util.Map; + + +class BatchImpl extends BaseDatastoreBatchWriter implements Batch { + + private final DatastoreImpl datastore; + private final boolean force; + + static class ResponseImpl implements Batch.Response { + + private final DatastoreV1.CommitResponse response; + + public ResponseImpl(DatastoreV1.CommitResponse response) { + this.response = response; + } + + @Override + public List generatedKeys() { + return Lists.transform(response.getMutationResult().getInsertAutoIdKeyList(), + new Function() { + @Override public Key apply(DatastoreV1.Key keyPb) { + return Key.fromPb(keyPb); + } + }); + } + } + + BatchImpl(DatastoreImpl datastore, BatchOption... options) { + super("batch"); + this.datastore = datastore; + Map, BatchOption> optionsMap = BatchOption.asImmutableMap(options); + if (optionsMap.containsKey(ForceWrites.class)) { + force = ((ForceWrites) optionsMap.get(ForceWrites.class)).force(); + } else { + force = datastore.options().force(); + } + } + + @Override + public Batch.Response submit() { + validateActive(); + DatastoreV1.Mutation.Builder mutationPb = toMutationPb(); + if (force) { + mutationPb.setForce(force); + } + DatastoreV1.CommitRequest.Builder requestPb = DatastoreV1.CommitRequest.newBuilder(); + requestPb.setMode(DatastoreV1.CommitRequest.Mode.NON_TRANSACTIONAL); + requestPb.setMutation(mutationPb); + DatastoreV1.CommitResponse responsePb = datastore.commit(requestPb.build()); + deactivate(); + return new ResponseImpl(responsePb); + } + + @Override + public Datastore datastore() { + return datastore; + } +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BatchOption.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BatchOption.java new file mode 100644 index 000000000000..362a74e96c79 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BatchOption.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +public abstract class BatchOption implements java.io.Serializable { + + private static final long serialVersionUID = -3932758377282659839L; + + public static final class ForceWrites extends BatchOption { + + private static final long serialVersionUID = 2555054296046232799L; + + private final boolean force; + + public ForceWrites(boolean force) { + this.force = force; + } + + public boolean force() { + return force; + } + } + + BatchOption() { + // package protected + } + + public static ForceWrites forceWrites() { + return new ForceWrites(true); + } + + static Map, BatchOption> asImmutableMap(BatchOption... options) { + ImmutableMap.Builder, BatchOption> builder = + ImmutableMap.builder(); + for (BatchOption option : options) { + builder.put(option.getClass(), option); + } + return builder.build(); + } +} diff --git a/src/main/java/com/google/gcloud/datastore/Blob.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Blob.java similarity index 78% rename from src/main/java/com/google/gcloud/datastore/Blob.java rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Blob.java index f1fabe2f4899..5a759240be38 100644 --- a/src/main/java/com/google/gcloud/datastore/Blob.java +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Blob.java @@ -1,6 +1,21 @@ +/* + * Copyright 2015 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.gcloud.datastore; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.services.datastore.DatastoreV1; @@ -18,7 +33,6 @@ /** * A Google Cloud Datastore Blob. - * A Datastore blob is limited to {@value #MAX_LENGTH} bytes. * This class is immutable. * * @see Google Cloud Datastore Entities, Properties, and Keys @@ -26,15 +40,11 @@ public final class Blob extends Serializable { private static final long serialVersionUID = 3835421019618247721L; - public static final int MAX_LENGTH = 1_000_000; private final transient ByteString byteString; - Blob(ByteString byteString, boolean enforceLimits) { + Blob(ByteString byteString) { this.byteString = checkNotNull(byteString); - if (enforceLimits) { - checkArgument(byteString.size() <= MAX_LENGTH, "May be a maximum of %,d bytes", MAX_LENGTH); - } } @Override @@ -57,13 +67,7 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Blob)) { - return false; - } - return byteString.equals(((Blob) obj).byteString); + return obj == this || obj instanceof Blob && byteString.equals(((Blob) obj).byteString); } /** @@ -93,7 +97,7 @@ public ByteBuffer asReadOnlyByteBuffer() { public InputStream asInputStream() { final ByteBuffer byteBuffer = asReadOnlyByteBuffer(); return new InputStream() { - @Override public int read() throws IOException { + @Override public int read() { return !byteBuffer.hasRemaining() ? -1 : byteBuffer.get() & 0xFF; } }; @@ -113,7 +117,7 @@ public void copyTo(ByteBuffer target) { /** * Copies bytes into a buffer. * - * @throws java.io.IndexOutOfBoundsException if an offset or size is negative or too large + * @throws IndexOutOfBoundsException if an offset or size is negative or too large */ public void copyTo(byte[] target) { byteString.copyTo(target, 0, 0, length()); @@ -124,11 +128,11 @@ ByteString byteString() { } public static Blob copyFrom(byte[] bytes) { - return new Blob(ByteString.copyFrom(bytes), true); + return new Blob(ByteString.copyFrom(bytes)); } public static Blob copyFrom(ByteBuffer bytes) { - return new Blob(ByteString.copyFrom(bytes), true); + return new Blob(ByteString.copyFrom(bytes)); } public static Blob copyFrom(InputStream input) throws IOException { @@ -148,6 +152,6 @@ protected Value toPb() { @Override protected Object fromPb(byte[] bytesPb) throws InvalidProtocolBufferException { - return new Blob(DatastoreV1.Value.parseFrom(bytesPb).getBlobValue(), false); + return new Blob(DatastoreV1.Value.parseFrom(bytesPb).getBlobValue()); } } diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BlobValue.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BlobValue.java new file mode 100644 index 000000000000..fb61c0b9ad34 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BlobValue.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import static com.google.api.services.datastore.DatastoreV1.Value.BLOB_VALUE_FIELD_NUMBER; + +import com.google.api.services.datastore.DatastoreV1; + +public final class BlobValue extends Value { + + private static final long serialVersionUID = -5096238337676649540L; + + static final BaseMarshaller MARSHALLER = + new BaseMarshaller() { + + private static final long serialVersionUID = -823515687083612387L; + + @Override + public int getProtoFieldId() { + return BLOB_VALUE_FIELD_NUMBER; + } + + @Override + public Builder newBuilder(Blob value) { + return builder(value); + } + + @Override + protected Blob getValue(DatastoreV1.Value from) { + return new Blob(from.getBlobValue()); + } + + @Override + protected void setValue(BlobValue from, DatastoreV1.Value.Builder to) { + to.setBlobValue(from.get().byteString()); + } + }; + + public static final class Builder extends Value.BaseBuilder { + + private Builder() { + super(ValueType.BLOB); + } + + @Override + public BlobValue build() { + return new BlobValue(this); + } + } + + public BlobValue(Blob blob) { + this(builder(blob)); + } + + private BlobValue(Builder builder) { + super(builder); + } + + @Override + public Builder toBuilder() { + return new Builder().mergeFrom(this); + } + + public static BlobValue of(Blob blob) { + return new BlobValue(blob); + } + + public static Builder builder(Blob blob) { + return new Builder().set(blob); + } +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BooleanValue.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BooleanValue.java new file mode 100644 index 000000000000..2dd98a5013ac --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/BooleanValue.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import static com.google.api.services.datastore.DatastoreV1.Value.BOOLEAN_VALUE_FIELD_NUMBER; + +import com.google.api.services.datastore.DatastoreV1; + +public final class BooleanValue extends Value { + + private static final long serialVersionUID = -542649497897250340L; + + static final BaseMarshaller MARSHALLER = + new BaseMarshaller() { + + private static final long serialVersionUID = 7080467411349092522L; + + @Override + public int getProtoFieldId() { + return BOOLEAN_VALUE_FIELD_NUMBER; + } + + @Override + public Builder newBuilder(Boolean value) { + return builder(value); + } + + @Override + protected Boolean getValue(DatastoreV1.Value from) { + return from.getBooleanValue(); + } + + @Override + protected void setValue(BooleanValue from, DatastoreV1.Value.Builder to) { + to.setBooleanValue(from.get()); + } + }; + + public static final class Builder extends Value.BaseBuilder { + + private Builder() { + super(ValueType.BOOLEAN); + } + + @Override + public BooleanValue build() { + return new BooleanValue(this); + } + } + + public BooleanValue(boolean value) { + this(builder(value)); + } + + private BooleanValue(Builder builder) { + super(builder); + } + + @Override + public Builder toBuilder() { + return new Builder().mergeFrom(this); + } + + public static BooleanValue of(boolean value) { + return new BooleanValue(value); + } + + public static Builder builder(boolean value) { + return new Builder().set(value); + } +} diff --git a/src/main/java/com/google/gcloud/datastore/Cursor.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Cursor.java similarity index 63% rename from src/main/java/com/google/gcloud/datastore/Cursor.java rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Cursor.java index e95f25144c12..42a8cee8e5a2 100644 --- a/src/main/java/com/google/gcloud/datastore/Cursor.java +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Cursor.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 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.gcloud.datastore; import static com.google.common.base.Preconditions.checkNotNull; @@ -7,8 +23,11 @@ import com.google.api.services.datastore.DatastoreV1.Value; import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects.ToStringHelper; +import com.google.common.base.Preconditions; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.TextFormat; +import com.google.protobuf.TextFormat.ParseException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -25,6 +44,7 @@ public final class Cursor extends Serializable { private final transient ByteString byteString; Cursor(ByteString byteString) { + Preconditions.checkArgument(byteString.isValidUtf8(), "content is not a valid UTF-8"); this.byteString = byteString; } @@ -35,13 +55,7 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Cursor)) { - return false; - } - return byteString.equals(((Cursor) obj).byteString); + return obj == this || obj instanceof Cursor && byteString.equals(((Cursor) obj).byteString); } @Override @@ -63,9 +77,9 @@ ByteString byteString() { */ public String toUrlSafe() { try { - return URLEncoder.encode(toPb().toString(), UTF_8.name()); + return URLEncoder.encode(TextFormat.printToString(toPb()), UTF_8.name()); } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Unxpeced encoding exception", e); + throw new IllegalStateException("Unexpected encoding exception", e); } } @@ -75,9 +89,11 @@ public String toUrlSafe() { public static Cursor fromUrlSafe(String urlSafe) { try { String utf8Str = URLDecoder.decode(urlSafe, UTF_8.name()); - return fromPb(DatastoreV1.Value.parseFrom(utf8Str.getBytes())); - } catch (UnsupportedEncodingException | InvalidProtocolBufferException e) { - throw new RuntimeException("Unxpeced decoding exception", e); + DatastoreV1.Value.Builder builder = DatastoreV1.Value.newBuilder(); + TextFormat.merge(utf8Str, builder); + return fromPb(builder.build()); + } catch (UnsupportedEncodingException | ParseException e) { + throw new IllegalStateException("Unexpected decoding exception", e); } } diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Datastore.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Datastore.java new file mode 100644 index 000000000000..fe79fdf45ff4 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Datastore.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import com.google.gcloud.Service; + +import java.util.List; + +/** + * An interface for Google Cloud Datastore. + */ +public interface Datastore extends Service, DatastoreReaderWriter { + + /** + * Returns a new Datastore transaction. + * + * @throws DatastoreException upon failure + */ + Transaction newTransaction(TransactionOption... options); + + + /** + * An Callback for running with a Transactional + * {@link com.google.gcloud.datastore.DatastoreReaderWriter}. + * The associated transaction will be committed after a successful return from the {@code run} + * method. Any propagated exception will cause the transaction to be rolled-back. + * + * @param the type of the return value + */ + interface TransactionCallable { + T run(DatastoreReaderWriter readerWriter) throws Exception; + } + + + /** + * Invokes the callback's {@link Datastore.TransactionCallable#run} method with a + * {@link DatastoreReaderWriter} that is associated with a new transaction. + * The transaction will be committed upon successful invocation. + * Any thrown exception will cause the transaction to rollback and will be propagated + * as a {@link DatastoreException} with the original exception as its root cause. + * + * @param callable the callback to call with a newly created transactional readerWriter + * @param options the options for the created transaction + * @throws DatastoreException upon failure + */ + T runInTransaction(TransactionCallable callable, TransactionOption... options); + + /** + * Returns a new Batch for processing multiple write operations in one request. + */ + Batch newBatch(BatchOption... options); + + /** + * Allocate a unique id for the given key. + * The returned key will have the same information (projectId, kind, namespace and ancestors) + * as the given key and will have a newly assigned id. + * + * @throws DatastoreException upon failure + */ + Key allocateId(IncompleteKey key); + + /** + * Returns a list of keys using the allocated ids ordered by the input. + * + * @throws DatastoreException upon failure + * @see #allocateId(IncompleteKey) + */ + List allocateId(IncompleteKey... key); + + /** + * {@inheritDoc} + * @throws DatastoreException upon failure + */ + @Override + void update(Entity... entity); + + /** + * {@inheritDoc} + * @throws DatastoreException upon failure + */ + @Override + void put(Entity... entity); + + /** + * {@inheritDoc} + * @throws DatastoreException upon failure + */ + @Override + void delete(Key... key); + + /** + * Returns a new KeyFactory for this service + */ + KeyFactory newKeyFactory(); +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreBatchWriter.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreBatchWriter.java new file mode 100644 index 000000000000..3a80452349dc --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreBatchWriter.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import java.util.List; + +/** + * An interface to represent a batch of write operations. + * All write operation for a batch writer will be applied to the Datastore in one RPC call. + */ +interface DatastoreBatchWriter extends DatastoreWriter { + + /** + * Datastore add operation. + * This method will also allocate id for any entity with an incomplete key. + * As oppose to {@link #add(FullEntity)}, this method will defer any necessary id allocation + * to submit time. + * + * @throws IllegalArgumentException if any of the given entities is missing a key + * @throws DatastoreException if a given entity with a + * complete key was already added to this writer or if not active + */ + void addWithDeferredIdAllocation(FullEntity... entity); + + /** + * {@inheritDoc} + * For entities with complete keys that were marked for deletion in this writer the operation + * will be changed to {@link #put}. + * @throws DatastoreException if a given entity with the + * same complete key was already added to this writer, if writer is not active or + * if id allocation for an entity with an incomplete key failed. + */ + @Override + List add(FullEntity... entity); + + /** + * {@inheritDoc} + * This operation will be converted to {@link #put} operation for entities that were already + * added or put in this writer + * @throws DatastoreException if an entity is marked for + * deletion in this writer or if not active + */ + @Override + void update(Entity... entity); + + /** + * {@inheritDoc} + * This operation will also remove from this batch any prior writes for entities with the same + * keys + * @throws DatastoreException if not active + */ + @Override + void delete(Key... key); + + /** + * {@inheritDoc} + * This operation will also remove from this writer any prior writes for the same entities. + * @throws DatastoreException if not active + */ + @Override + void put(Entity... entity); + + /** + * Returns {@code true} if still active (write operations were not sent to the Datastore). + */ + boolean active(); +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreException.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreException.java new file mode 100644 index 000000000000..d91cc2ccd98b --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreException.java @@ -0,0 +1,155 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; +import com.google.gcloud.RetryHelper; +import com.google.gcloud.RetryHelper.RetryHelperException; +import com.google.gcloud.spi.DatastoreRpc.DatastoreRpcException; +import com.google.gcloud.spi.DatastoreRpc.DatastoreRpcException.Reason; + +import java.util.HashMap; +import java.util.Map; + +public class DatastoreException extends RuntimeException { + + private static final long serialVersionUID = 8170357898917041899L; + private static final ImmutableMap REASON_TO_CODE; + private static final ImmutableMap HTTP_TO_CODE; + + private final Code code; + + /** + * An error code to represent the failure. + * + * @see Google Cloud Datastore error codes + */ + public enum Code { + + ABORTED(Reason.ABORTED), + DEADLINE_EXCEEDED(Reason.DEADLINE_EXCEEDED), + UNAVAILABLE(Reason.UNAVAILABLE), + FAILED_PRECONDITION(Reason.FAILED_PRECONDITION), + INVALID_ARGUMENT(Reason.INVALID_ARGUMENT), + PERMISSION_DENIED(Reason.PERMISSION_DENIED), + UNAUTHORIZED(false, "Unauthorized", 401), + INTERNAL(Reason.INTERNAL), + RESOURCE_EXHAUSTED(Reason.RESOURCE_EXHAUSTED), + UNKNOWN(false, "Unknown failure", -1); + + private final boolean retryable; + private final String description; + private final int httpStatus; + + Code(Reason reason) { + this(reason.retryable(), reason.description(), reason.httpStatus()); + } + + Code(boolean retryable, String description, int httpStatus) { + this.retryable = retryable; + this.description = description; + this.httpStatus = httpStatus; + } + + public String description() { + return description; + } + + public int httpStatus() { + return httpStatus; + } + + /** + * Returns {@code true} if this exception is transient and the same request could be retried. + * For any retry it is highly recommended to apply an exponential backoff. + */ + public boolean retryable() { + return retryable; + } + + DatastoreException translate(DatastoreRpcException exception, String message) { + return new DatastoreException(this, message, exception); + } + } + + static { + ImmutableMap.Builder builder = ImmutableMap.builder(); + Map httpCodes = new HashMap<>(); + for (Code code : Code.values()) { + builder.put(code.name(), code); + httpCodes.put(code.httpStatus(), code); + } + REASON_TO_CODE = builder.build(); + HTTP_TO_CODE = ImmutableMap.copyOf(httpCodes); + } + + public DatastoreException(Code code, String message, Exception cause) { + super(MoreObjects.firstNonNull(message, code.description), cause); + this.code = code; + } + + public DatastoreException(Code code, String message) { + this(code, message, null); + } + + /** + * Returns the code associated with this exception. + */ + public Code code() { + return code; + } + + static DatastoreException translateAndThrow(RetryHelperException ex) { + if (ex.getCause() instanceof DatastoreRpcException) { + return translateAndThrow((DatastoreRpcException) ex.getCause()); + } + if (ex instanceof RetryHelper.RetryInterruptedException) { + RetryHelper.RetryInterruptedException.propagate(); + } + throw new DatastoreException(Code.UNKNOWN, ex.getMessage(), ex); + } + + /** + * Translate DatastoreException to DatastoreException based on their + * HTTP error codes. This method will always throw a new DatastoreException. + * + * @throws DatastoreException every time + */ + static DatastoreException translateAndThrow(DatastoreRpcException exception) { + String message = exception.getMessage(); + Code code = REASON_TO_CODE.get(exception.reason()); + if (code == null) { + code = MoreObjects.firstNonNull(HTTP_TO_CODE.get(exception.httpStatus()), Code.UNKNOWN); + } + throw code.translate(exception, message); + } + + /** + * Throw a DatastoreException with {@code FAILED_PRECONDITION} code and the {@code message} + * in a nested exception. + * + * @throws DatastoreException every time + */ + static DatastoreException throwInvalidRequest(String massage, Object... params) { + throw new DatastoreException(Code.FAILED_PRECONDITION, String.format(massage, params)); + } + + static DatastoreException propagateUserException(Exception ex) { + throw new DatastoreException(Code.UNKNOWN, ex.getMessage(), ex); + } +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreFactory.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreFactory.java new file mode 100644 index 000000000000..a64fab3715f1 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 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.gcloud.datastore; + + +/** + * A base class for Datastore factories. + */ +public abstract class DatastoreFactory { + + private static final DatastoreFactory INSTANCE = new DatastoreFactory() { + @Override + public Datastore get(DatastoreOptions options) { + return new DatastoreImpl(options); + } + }; + + /** + * Returns the default factory instance. + */ + public static DatastoreFactory instance() { + return INSTANCE; + } + + /** + * Returns a {@code Datastore} service for the given options. + */ + public abstract Datastore get(DatastoreOptions options); +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreHelper.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreHelper.java new file mode 100644 index 000000000000..a74d06642740 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreHelper.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import com.google.common.collect.Iterators; +import com.google.common.collect.Maps; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Provide functionality that should be added to the appropriate interfaces + * via Java 8 default methods. + */ +class DatastoreHelper { + + private DatastoreHelper() { + } + + + static Key allocateId(Datastore service, IncompleteKey key) { + return service.allocateId(new IncompleteKey[]{key}).get(0); + } + + static Entity get(DatastoreReader reader, Key key) { + return Iterators.getNext(reader.get(new Key[]{key}), null); + } + + static Entity add(DatastoreWriter writer, FullEntity entity) { + return writer.add(new FullEntity[] {entity}).get(0); + } + + static KeyFactory newKeyFactory(DatastoreOptions options) { + return new KeyFactory(options.projectId(), options.namespace()); + } + + /** + * Returns a list with a value for each given key (ordered by input). + * A {@code null} would be returned for non-existing keys. + */ + static List fetch(DatastoreReader reader, Key... keys) { + Iterator entities = reader.get(keys); + Map map = Maps.newHashMapWithExpectedSize(keys.length); + while (entities.hasNext()) { + Entity entity = entities.next(); + map.put(entity.key(), entity); + } + List list = new ArrayList<>(keys.length); + for (Key key : keys) { + // this will include nulls for non-existing keys + list.add(map.get(key)); + } + return list; + } + + static T runInTransaction(Datastore datastore, + Datastore.TransactionCallable callable, TransactionOption... options) { + Transaction transaction = datastore.newTransaction(options); + try { + T value = callable.run(transaction); + transaction.commit(); + return value; + } catch (Exception ex) { + transaction.rollback(); + throw DatastoreException.propagateUserException(ex); + } finally { + if (transaction.active()) { + transaction.rollback(); + } + } + } +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreImpl.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreImpl.java new file mode 100644 index 000000000000..e848dd5e56c8 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreImpl.java @@ -0,0 +1,391 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import com.google.api.services.datastore.DatastoreV1; +import com.google.common.base.Function; +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterators; +import com.google.common.collect.Sets; +import com.google.gcloud.BaseService; +import com.google.gcloud.ExceptionHandler; +import com.google.gcloud.ExceptionHandler.Interceptor; +import com.google.gcloud.RetryHelper; +import com.google.gcloud.RetryHelper.RetryHelperException; +import com.google.gcloud.RetryParams; +import com.google.gcloud.spi.DatastoreRpc; +import com.google.gcloud.spi.DatastoreRpc.DatastoreRpcException; +import com.google.protobuf.ByteString; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; + + +final class DatastoreImpl extends BaseService + implements Datastore { + + private static final Interceptor EXCEPTION_HANDLER_INTERCEPTOR = + new Interceptor() { + + private static final long serialVersionUID = 6911242958397733203L; + + @Override + public RetryResult afterEval(Exception exception, RetryResult retryResult) { + return null; + } + + @Override + public RetryResult beforeEval(Exception exception) { + if (exception instanceof DatastoreRpcException) { + boolean retryable = ((DatastoreRpcException) exception).retryable(); + return retryable ? Interceptor.RetryResult.RETRY : Interceptor.RetryResult.ABORT; + } + return null; + } + }; + private static final ExceptionHandler EXCEPTION_HANDLER = ExceptionHandler.builder() + .abortOn(RuntimeException.class, DatastoreRpcException.class) + .interceptor(EXCEPTION_HANDLER_INTERCEPTOR).build(); + + private final DatastoreRpc datastoreRpc; + private final RetryParams retryParams; + + DatastoreImpl(DatastoreOptions options) { + super(options); + this.datastoreRpc = options.datastoreRpc(); + retryParams = MoreObjects.firstNonNull(options.retryParams(), RetryParams.noRetries()); + } + + @Override + public Batch newBatch(BatchOption... options) { + return new BatchImpl(this, options); + } + + @Override + public Transaction newTransaction(TransactionOption... options) { + return new TransactionImpl(this, options); + } + + @Override + public T runInTransaction(TransactionCallable callable, TransactionOption... options) { + return DatastoreHelper.runInTransaction(this, callable, options); + } + + @Override + public QueryResults run(Query query) { + return run(null, query); + } + + QueryResults run(DatastoreV1.ReadOptions readOptionsPb, Query query) { + return new QueryResultsImpl<>(this, readOptionsPb, query); + } + + DatastoreV1.RunQueryResponse runQuery(final DatastoreV1.RunQueryRequest requestPb) { + try { + return RetryHelper.runWithRetries(new Callable() { + @Override public DatastoreV1.RunQueryResponse call() throws DatastoreRpcException { + return datastoreRpc.runQuery(requestPb); + } + }, retryParams, EXCEPTION_HANDLER); + } catch (RetryHelperException e) { + throw DatastoreException.translateAndThrow(e); + } + } + + @Override + public Key allocateId(IncompleteKey key) { + return DatastoreHelper.allocateId(this, key); + } + + @Override + public List allocateId(IncompleteKey... keys) { + if (keys.length == 0) { + return Collections.emptyList(); + } + DatastoreV1.AllocateIdsRequest.Builder requestPb = DatastoreV1.AllocateIdsRequest.newBuilder(); + for (IncompleteKey key : keys) { + requestPb.addKey(trimNameOrId(key).toPb()); + } + DatastoreV1.AllocateIdsResponse responsePb = allocateIds(requestPb.build()); + Iterator keyIterator = responsePb.getKeyList().iterator(); + ImmutableList.Builder builder = ImmutableList.builder().addAll( + Iterators.transform(keyIterator, new Function() { + @Override + public Key apply(DatastoreV1.Key keyPb) { + return Key.fromPb(keyPb); + } + })); + return builder.build(); + } + + DatastoreV1.AllocateIdsResponse allocateIds(final DatastoreV1.AllocateIdsRequest requestPb) { + try { + return RetryHelper.runWithRetries(new Callable() { + @Override public DatastoreV1.AllocateIdsResponse call() throws DatastoreRpcException { + return datastoreRpc.allocateIds(requestPb); + } + }, retryParams, EXCEPTION_HANDLER); + } catch (RetryHelperException e) { + throw DatastoreException.translateAndThrow(e); + } + } + + private IncompleteKey trimNameOrId(IncompleteKey key) { + if (key instanceof Key) { + return IncompleteKey.builder(key).build(); + } + return key; + } + + @Override + public Entity add(FullEntity entity) { + return DatastoreHelper.add(this, entity); + } + + @SuppressWarnings("unchecked") + @Override + public List add(FullEntity... entities) { + if (entities.length == 0) { + return Collections.emptyList(); + } + DatastoreV1.Mutation.Builder mutationPb = DatastoreV1.Mutation.newBuilder(); + Map completeEntities = new LinkedHashMap<>(); + for (FullEntity entity : entities) { + Entity completeEntity = null; + if (entity.key() instanceof Key) { + completeEntity = Entity.convert((FullEntity) entity); + } + if (completeEntity != null) { + if (completeEntities.put(completeEntity.key(), completeEntity) != null) { + throw DatastoreException.throwInvalidRequest( + "Duplicate entity with the key %s", entity.key()); + } + mutationPb.addInsert(completeEntity.toPb()); + } else { + Preconditions.checkArgument(entity.hasKey(), "entity %s is missing a key", entity); + mutationPb.addInsertAutoId(entity.toPb()); + } + } + DatastoreV1.CommitResponse commitResponse = commitMutation(mutationPb); + Iterator allocatedKeys = + commitResponse.getMutationResult().getInsertAutoIdKeyList().iterator(); + ImmutableList.Builder responseBuilder = ImmutableList.builder(); + for (FullEntity entity : entities) { + Entity completeEntity = completeEntities.get(entity.key()); + if (completeEntity != null) { + responseBuilder.add(completeEntity); + } else { + responseBuilder.add(Entity.builder(Key.fromPb(allocatedKeys.next()), entity).build()); + } + } + return responseBuilder.build(); + } + + @Override + public Entity get(Key key) { + return DatastoreHelper.get(this, key); + } + + @Override + public Iterator get(Key... keys) { + return get(null, keys); + } + + @Override + public List fetch(Key... keys) { + return DatastoreHelper.fetch(this, keys); + } + + Iterator get(DatastoreV1.ReadOptions readOptionsPb, final Key... keys) { + if (keys.length == 0) { + return Collections.emptyIterator(); + } + DatastoreV1.LookupRequest.Builder requestPb = DatastoreV1.LookupRequest.newBuilder(); + if (readOptionsPb != null) { + requestPb.setReadOptions(readOptionsPb); + } + for (Key k : Sets.newLinkedHashSet(Arrays.asList(keys))) { + requestPb.addKey(k.toPb()); + } + return new ResultsIterator(requestPb); + } + + final class ResultsIterator extends AbstractIterator { + + private final DatastoreV1.LookupRequest.Builder requestPb; + Iterator iter; + + ResultsIterator(DatastoreV1.LookupRequest.Builder requestPb) { + this.requestPb = requestPb; + loadResults(); + } + + private void loadResults() { + DatastoreV1.LookupResponse responsePb = lookup(requestPb.build()); + iter = responsePb.getFoundList().iterator(); + requestPb.clearKey(); + if (responsePb.getDeferredCount() > 0) { + requestPb.addAllKey(responsePb.getDeferredList()); + } + } + + @SuppressWarnings("unchecked") + @Override + protected Entity computeNext() { + if (iter.hasNext()) { + return Entity.fromPb(iter.next().getEntity()); + } + while (!iter.hasNext()) { + if (requestPb.getKeyCount() == 0) { + return endOfData(); + } + loadResults(); + } + return Entity.fromPb(iter.next().getEntity()); + } + } + + DatastoreV1.LookupResponse lookup(final DatastoreV1.LookupRequest requestPb) { + try { + return RetryHelper.runWithRetries(new Callable() { + @Override public DatastoreV1.LookupResponse call() throws DatastoreRpcException { + return datastoreRpc.lookup(requestPb); + } + }, retryParams, EXCEPTION_HANDLER); + } catch (RetryHelperException e) { + throw DatastoreException.translateAndThrow(e); + } + } + + @SafeVarargs + @Override + public final void update(Entity... entities) { + if (entities.length > 0) { + DatastoreV1.Mutation.Builder mutationPb = DatastoreV1.Mutation.newBuilder(); + Map dedupEntities = new LinkedHashMap<>(); + for (Entity entity : entities) { + dedupEntities.put(entity.key(), entity); + } + for (Entity entity : dedupEntities.values()) { + mutationPb.addUpdate(entity.toPb()); + } + commitMutation(mutationPb); + } + } + + @SafeVarargs + @Override + public final void put(Entity... entities) { + if (entities.length > 0) { + DatastoreV1.Mutation.Builder mutationPb = DatastoreV1.Mutation.newBuilder(); + Map dedupEntities = new LinkedHashMap<>(); + for (Entity entity : entities) { + dedupEntities.put(entity.key(), entity); + } + for (Entity e : dedupEntities.values()) { + mutationPb.addUpsert(e.toPb()); + } + commitMutation(mutationPb); + } + } + + @Override + public void delete(Key... keys) { + if (keys.length > 0) { + DatastoreV1.Mutation.Builder mutationPb = DatastoreV1.Mutation.newBuilder(); + Set dedupKeys = new LinkedHashSet<>(Arrays.asList(keys)); + for (Key key : dedupKeys) { + mutationPb.addDelete(key.toPb()); + } + commitMutation(mutationPb); + } + } + + @Override + public KeyFactory newKeyFactory() { + return DatastoreHelper.newKeyFactory(options()); + } + + private DatastoreV1.CommitResponse commitMutation(DatastoreV1.Mutation.Builder mutationPb) { + if (options().force()) { + mutationPb.setForce(true); + } + DatastoreV1.CommitRequest.Builder requestPb = DatastoreV1.CommitRequest.newBuilder(); + requestPb.setMode(DatastoreV1.CommitRequest.Mode.NON_TRANSACTIONAL); + requestPb.setMutation(mutationPb); + return commit(requestPb.build()); + } + + DatastoreV1.CommitResponse commit(final DatastoreV1.CommitRequest requestPb) { + try { + return RetryHelper.runWithRetries(new Callable() { + @Override public DatastoreV1.CommitResponse call() throws DatastoreRpcException { + return datastoreRpc.commit(requestPb); + } + }, retryParams, EXCEPTION_HANDLER); + } catch (RetryHelperException e) { + throw DatastoreException.translateAndThrow(e); + } + } + + ByteString requestTransactionId(DatastoreV1.BeginTransactionRequest.Builder requestPb) { + return beginTransaction(requestPb.build()).getTransaction(); + } + + DatastoreV1.BeginTransactionResponse beginTransaction( + final DatastoreV1.BeginTransactionRequest requestPb) { + try { + return RetryHelper.runWithRetries(new Callable() { + @Override + public DatastoreV1.BeginTransactionResponse call() throws DatastoreRpcException { + return datastoreRpc.beginTransaction(requestPb); + } + }, retryParams, EXCEPTION_HANDLER); + } catch (RetryHelperException e) { + throw DatastoreException.translateAndThrow(e); + } + } + + void rollbackTransaction(ByteString transaction) { + DatastoreV1.RollbackRequest.Builder requestPb = DatastoreV1.RollbackRequest.newBuilder(); + requestPb.setTransaction(transaction); + rollback(requestPb.build()); + } + + void rollback(final DatastoreV1.RollbackRequest requestPb) { + try { + RetryHelper.runWithRetries(new Callable() { + @Override public Void call() throws DatastoreRpcException { + datastoreRpc.rollback(requestPb); + return null; + } + }, retryParams, EXCEPTION_HANDLER); + } catch (RetryHelperException e) { + throw DatastoreException.translateAndThrow(e); + } + } +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreOptions.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreOptions.java new file mode 100644 index 000000000000..ed6b51458938 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreOptions.java @@ -0,0 +1,204 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import static com.google.gcloud.datastore.Validator.validateNamespace; + +import com.google.api.services.datastore.DatastoreV1; +import com.google.api.services.datastore.DatastoreV1.EntityResult; +import com.google.api.services.datastore.DatastoreV1.LookupResponse; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.gcloud.ServiceOptions; +import com.google.gcloud.spi.DatastoreRpc; +import com.google.gcloud.spi.DatastoreRpc.DatastoreRpcException; +import com.google.gcloud.spi.DatastoreRpcFactory; +import com.google.gcloud.spi.DefaultDatastoreRpc; + +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.Objects; +import java.util.Set; + +public class DatastoreOptions extends ServiceOptions { + + private static final long serialVersionUID = -8636602944160689193L; + private static final String DATASET_ENV_NAME = "DATASTORE_DATASET"; + private static final String DATASTORE_SCOPE = "https://www.googleapis.com/auth/datastore"; + private static final String USERINFO_SCOPE = "https://www.googleapis.com/auth/userinfo.email"; + private static final Set SCOPES = ImmutableSet.of(DATASTORE_SCOPE, USERINFO_SCOPE); + + private final String namespace; + private final boolean force; + private final boolean normalizeDataset; + private transient DatastoreRpc datastoreRpc; + + public static class Builder extends + ServiceOptions.Builder { + + private String namespace; + private boolean force; + private boolean normalizeDataset = true; + + private Builder() { + } + + private Builder(DatastoreOptions options) { + super(options); + force = options.force; + namespace = options.namespace; + normalizeDataset = options.normalizeDataset; + } + + @Override + public DatastoreOptions build() { + DatastoreOptions options = new DatastoreOptions(this); + return normalizeDataset ? options.normalize() : options; + } + + public Builder namespace(String namespace) { + this.namespace = validateNamespace(namespace); + return this; + } + + public Builder force(boolean force) { + this.force = force; + return this; + } + + Builder normalizeDataset(boolean normalizeDataset) { + this.normalizeDataset = normalizeDataset; + return this; + } + } + + private DatastoreOptions(Builder builder) { + super(builder); + normalizeDataset = builder.normalizeDataset; + namespace = builder.namespace != null ? builder.namespace : defaultNamespace(); + force = builder.force; + } + + private DatastoreOptions normalize() { + if (!normalizeDataset) { + return this; + } + + Builder builder = toBuilder(); + builder.normalizeDataset(false); + // Replace provided project-id with full project-id (s~xxx, e~xxx,...) + DatastoreV1.LookupRequest.Builder requestPb = DatastoreV1.LookupRequest.newBuilder(); + DatastoreV1.Key key = DatastoreV1.Key.newBuilder() + .addPathElement(DatastoreV1.Key.PathElement.newBuilder().setKind("__foo__").setName("bar")) + .build(); + requestPb.addKey(key); + try { + LookupResponse responsePb = datastoreRpc().lookup(requestPb.build()); + if (responsePb.getDeferredCount() > 0) { + key = responsePb.getDeferred(0); + } else { + Iterator combinedIter = + Iterables.concat(responsePb.getMissingList(), responsePb.getFoundList()).iterator(); + key = combinedIter.next().getEntity().getKey(); + } + builder.projectId(key.getPartitionId().getDatasetId()); + return new DatastoreOptions(builder); + } catch (DatastoreRpcException e) { + throw DatastoreException.translateAndThrow(e); + } + } + + @Override + protected String defaultProject() { + String projectId = System.getProperty(DATASET_ENV_NAME, System.getenv(DATASET_ENV_NAME)); + if (projectId == null) { + projectId = appEngineAppId(); + } + return projectId != null ? projectId : super.defaultProject(); + } + + public String namespace() { + return namespace; + } + + private static String defaultNamespace() { + // TODO(ozarov): An alternative to reflection would be to depend on AE api jar: + // http://mvnrepository.com/artifact/com.google.appengine/appengine-api-1.0-sdk/1.2.0 + try { + Class clazz = Class.forName("com.google.appengine.api.NamespaceManager"); + Method method = clazz.getMethod("get"); + String namespace = (String) method.invoke(null); + return namespace == null || namespace.isEmpty() ? null : namespace; + } catch (Exception ignore) { + // return null (Datastore default namespace) if could not automatically determine + return null; + } + } + + public boolean force() { + return force; + } + + @Override + protected Set scopes() { + return SCOPES; + } + + @Override + public Builder toBuilder() { + return new Builder(this); + } + + @Override + public int hashCode() { + return super.hashCode() ^ Objects.hash(namespace, force, normalizeDataset); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DatastoreOptions)) { + return false; + } + DatastoreOptions other = (DatastoreOptions) obj; + return isEquals(other) && Objects.equals(namespace, other.namespace) + && Objects.equals(force, other.force) + && Objects.equals(normalizeDataset, other.normalizeDataset); + } + + DatastoreRpc datastoreRpc() { + if (datastoreRpc != null) { + return datastoreRpc; + } + if (serviceRpcFactory() != null) { + datastoreRpc = serviceRpcFactory().create(this); + } else { + datastoreRpc = createRpc(this, DatastoreRpcFactory.class); + if (datastoreRpc == null) { + datastoreRpc = new DefaultDatastoreRpc(this); + } + } + return datastoreRpc; + } + + public static DatastoreOptions defaultInstance() { + return builder().build(); + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreReader.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreReader.java new file mode 100644 index 000000000000..056895f850e3 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreReader.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import java.util.Iterator; +import java.util.List; + +/** + * An interface to represent Google Cloud Datastore read operations. + */ +public interface DatastoreReader { + + /** + * Returns an {@link Entity} for the given {@link Key} or {@code null} if does not exists. + * + * @throws DatastoreException upon failure. + */ + Entity get(Key key); + + /** + * Returns an {@link Entity} for each given {@link Key} that exists in the Datastore. + * The order of the result is unspecified. + * Results are loaded lazily therefore it is possible to get a {@code DatastoreException} + * from the returned {@code Iterator}'s {@link Iterator#hasNext hasNext} or + * {@link Iterator#next next} methods. + * + * @throws DatastoreException upon failure. + * @see #get(Key) + */ + Iterator get(Key... key); + + /** + * Returns a list with a value for each given key (ordered by input). + * A {@code null} would be returned for non-existing keys. + * When possible prefer using {@link #get(Key...)} which does not eagerly loads the results. + */ + List fetch(Key... keys); + + /** + * Submit a {@link Query} and returns its result. + * + * @throws DatastoreException upon failure. + */ + QueryResults run(Query query); +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreReaderWriter.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreReaderWriter.java new file mode 100644 index 000000000000..c64f86a8d0a3 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreReaderWriter.java @@ -0,0 +1,24 @@ +/* + * Copyright 2015 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.gcloud.datastore; + + +/** + * An interface that combines both Google Cloud Datastore read and write operations. + */ +public interface DatastoreReaderWriter extends DatastoreReader, DatastoreWriter { +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreWriter.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreWriter.java new file mode 100644 index 000000000000..66ba98aed9e9 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DatastoreWriter.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import java.util.List; + +/** + * An interface to represent Google Cloud Datastore write operations. + */ +public interface DatastoreWriter { + + /** + * Datastore add operation. + * This method will automatically allocate an id if necessary. + * + * @param entity the entity to add + * @return an {@code Entity} with the same properties and a key that is either newly allocated + * or the same one if key is already complete + * @throws DatastoreException upon failure + * @throws IllegalArgumentException if the given entity is missing a key + */ + Entity add(FullEntity entity); + + /** + * Datastore add operation. + * This method will automatically allocate id for any entity with an incomplete key. + * + * @return a list of {@code Entity} ordered by input with the same properties and a key that + * is either newly allocated or the same one if was already complete + * @throws DatastoreException upon failure + * @throws IllegalArgumentException if any of the given entities is missing a key + * @see #add(FullEntity) + */ + List add(FullEntity... entity); + + /** + * A Datastore update operation. + * The operation will fail if an entity with the same key does not already exist. + */ + void update(Entity... entity); + + /** + * A Datastore put (a.k.a upsert) operation. + * The operation will add or modify the entities. + */ + void put(Entity... entity); + + /** + * A datastore delete operation. + * It is OK request a deletion of a non-existing entity. + */ + void delete(Key... key); +} diff --git a/src/main/java/com/google/gcloud/datastore/DateTime.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DateTime.java similarity index 69% rename from src/main/java/com/google/gcloud/datastore/DateTime.java rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DateTime.java index e04d603ff031..853856c1b696 100644 --- a/src/main/java/com/google/gcloud/datastore/DateTime.java +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DateTime.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 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.gcloud.datastore; import static com.google.common.base.Preconditions.checkNotNull; @@ -17,7 +33,8 @@ * * @see Google Cloud Datastore Entities, Properties, and Keys */ -public final class DateTime extends Serializable { +public final class DateTime extends Serializable + implements Comparable { private static final long serialVersionUID = 7343324797621228378L; @@ -37,15 +54,15 @@ public int hashCode() { return (int) timestampMicroseconds; } + @Override + public int compareTo(DateTime other) { + return toDate().compareTo(other.toDate()); + } + @Override public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof DateTime)) { - return false; - } - return timestampMicroseconds == ((DateTime) obj).timestampMicroseconds; + return obj == this || obj instanceof DateTime + && timestampMicroseconds == ((DateTime) obj).timestampMicroseconds; } public long timestampMicroseconds() { @@ -67,7 +84,7 @@ public Calendar toCalendar() { } public static DateTime now() { - return new DateTime(System.nanoTime() / 1000L); + return copyFrom(new Date()); } public static DateTime copyFrom(Date date) { diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DateTimeValue.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DateTimeValue.java new file mode 100644 index 000000000000..7aec5c7d3c47 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DateTimeValue.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import static com.google.api.services.datastore.DatastoreV1.Value.TIMESTAMP_MICROSECONDS_VALUE_FIELD_NUMBER; + +import com.google.api.services.datastore.DatastoreV1; + +public final class DateTimeValue extends Value { + + private static final long serialVersionUID = -5096238337676649540L; + + static final BaseMarshaller MARSHALLER = + new BaseMarshaller() { + + private static final long serialVersionUID = -5695812592049332840L; + + @Override + public int getProtoFieldId() { + return TIMESTAMP_MICROSECONDS_VALUE_FIELD_NUMBER; + } + + @Override + public Builder newBuilder(DateTime value) { + return builder(value); + } + + @Override + protected DateTime getValue(DatastoreV1.Value from) { + return new DateTime(from.getTimestampMicrosecondsValue()); + } + + @Override + protected void setValue(DateTimeValue from, DatastoreV1.Value.Builder to) { + to.setTimestampMicrosecondsValue(from.get().timestampMicroseconds()); + } + }; + + public static final class Builder extends Value.BaseBuilder { + + private Builder() { + super(ValueType.DATE_TIME); + } + + @Override + public DateTimeValue build() { + return new DateTimeValue(this); + } + } + + public DateTimeValue(DateTime dateTime) { + this(builder(dateTime)); + } + + private DateTimeValue(Builder builder) { + super(builder); + } + + @Override + public Builder toBuilder() { + return new Builder().mergeFrom(this); + } + + public static DateTimeValue of(DateTime dateTime) { + return new DateTimeValue(dateTime); + } + + public static Builder builder(DateTime dateTime) { + return new Builder().set(dateTime); + } +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DoubleValue.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DoubleValue.java new file mode 100644 index 000000000000..d12bbe317aef --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/DoubleValue.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import static com.google.api.services.datastore.DatastoreV1.Value.DOUBLE_VALUE_FIELD_NUMBER; + +import com.google.api.services.datastore.DatastoreV1; + +public final class DoubleValue extends Value { + + private static final long serialVersionUID = -5096238337676649540L; + + static final BaseMarshaller MARSHALLER = + new BaseMarshaller() { + + private static final long serialVersionUID = 3935522813529400538L; + + @Override + public int getProtoFieldId() { + return DOUBLE_VALUE_FIELD_NUMBER; + } + + @Override + public Builder newBuilder(Double value) { + return builder(value); + } + + @Override + protected Double getValue(DatastoreV1.Value from) { + return from.getDoubleValue(); + } + + @Override + protected void setValue(DoubleValue from, DatastoreV1.Value.Builder to) { + to.setDoubleValue(from.get()); + } + }; + + public static final class Builder extends Value.BaseBuilder { + + public Builder() { + super(ValueType.DOUBLE); + } + + @Override + public DoubleValue build() { + return new DoubleValue(this); + } + } + + public DoubleValue(double value) { + this(builder(value)); + } + + private DoubleValue(Builder builder) { + super(builder); + } + + @Override + public Builder toBuilder() { + return new Builder().mergeFrom(this); + } + + public static DoubleValue of(double value) { + return new DoubleValue(value); + } + + public static Builder builder(double value) { + return new Builder().set(value); + } +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Entity.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Entity.java new file mode 100644 index 000000000000..7842fba61f0c --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Entity.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.services.datastore.DatastoreV1; +import com.google.common.base.Preconditions; + +/** + * An entity is the Google Cloud Datastore persistent data object for a specific key. + * An entity will always have a complete {@link Key}. + */ +public final class Entity extends FullEntity { + + private static final long serialVersionUID = 432961565733066915L; + + public static final class Builder extends BaseEntity.Builder { + + private Builder() { + } + + private Builder(Key key) { + super(checkNotNull(key)); + } + + private Builder(Entity entity) { + super(entity); + } + + private Builder(Key key, FullEntity entity) { + properties(entity.properties()); + key(key); + } + + @Override + public Builder key(Key key) { + super.key(checkNotNull(key)); + return this; + } + + @Override + public Entity build() { + Preconditions.checkState(key() != null); + return new Entity(this); + } + } + + Entity(Builder builder) { + super(builder); + } + + Entity(FullEntity from) { + super(from); + Preconditions.checkArgument(from.key() != null); + } + + @Override + protected BaseEntity.Builder emptyBuilder() { + return new Builder(); + } + + static Entity convert(FullEntity from) { + if (from instanceof Entity) { + return (Entity) from; + } + return new Entity(from); + } + + public static Builder builder(Key key) { + return new Builder(key); + } + + public static Builder builder(Entity copyFrom) { + return new Builder(copyFrom); + } + + public static Builder builder(Key key, FullEntity copyFrom) { + return new Builder(key, copyFrom); + } + + static Entity fromPb(DatastoreV1.Entity entityPb) { + return new Builder().fill(entityPb).build(); + } +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/EntityValue.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/EntityValue.java new file mode 100644 index 000000000000..add50e1747b3 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/EntityValue.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import static com.google.api.services.datastore.DatastoreV1.Value.ENTITY_VALUE_FIELD_NUMBER; + +import com.google.api.services.datastore.DatastoreV1; +import com.google.common.base.Preconditions; + +public class EntityValue extends Value { + + private static final long serialVersionUID = -5461475706792576395L; + + static final BaseMarshaller MARSHALLER = + new BaseMarshaller() { + + private static final long serialVersionUID = 2355075086076070931L; + + @Override + public int getProtoFieldId() { + return ENTITY_VALUE_FIELD_NUMBER; + } + + @Override + public Builder newBuilder(FullEntity value) { + return builder(value); + } + + @Override + protected FullEntity getValue(DatastoreV1.Value from) { + return FullEntity.fromPb(from.getEntityValue()); + } + + @Override + protected void setValue(EntityValue from, DatastoreV1.Value.Builder to) { + to.setEntityValue(from.get().toPb()); + } + }; + + public static final class Builder extends Value.BaseBuilder { + + private Builder() { + super(ValueType.ENTITY); + } + + @Override + public Builder indexed(boolean indexed) { + // see issue #25 + Preconditions.checkArgument(!indexed, "EntityValue can't be indexed"); + return super.indexed(indexed); + } + + @Override + public EntityValue build() { + return new EntityValue(this); + } + } + + public EntityValue(FullEntity entity) { + this(builder(entity)); + } + + private EntityValue(Builder builder) { + super(builder); + } + + @Override + public Builder toBuilder() { + return new Builder().mergeFrom(this); + } + + public static EntityValue of(FullEntity entity) { + return new EntityValue(entity); + } + + public static Builder builder(FullEntity entity) { + return new Builder().set(entity).indexed(false); + } +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/FullEntity.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/FullEntity.java new file mode 100644 index 000000000000..d7084420665e --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/FullEntity.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import com.google.api.services.datastore.DatastoreV1; + +/** + * A full entity is a {@link BaseEntity} that with a complete set of properties. + */ +public class FullEntity extends BaseEntity { + + private static final long serialVersionUID = 432961565733066915L; + + public static class Builder extends BaseEntity.Builder> { + + Builder() { + } + + Builder(K key) { + super(key); + } + + Builder(FullEntity entity) { + super(entity); + } + + @Override + public FullEntity build() { + return new FullEntity<>(this); + } + } + + FullEntity(BaseEntity.Builder builder) { + super(builder); + } + + FullEntity(FullEntity from) { + super(from); + } + + @Override + protected BaseEntity.Builder emptyBuilder() { + return new Builder(); + } + + public static Builder builder() { + return new Builder<>(); + } + + public static Builder builder(K key) { + return new Builder<>(key); + } + + public static Builder builder(FullEntity copyFrom) { + return new Builder<>(copyFrom); + } + + + static FullEntity fromPb(DatastoreV1.Entity entityPb) { + return new Builder<>().fill(entityPb).build(); + } +} diff --git a/src/main/java/com/google/gcloud/datastore/GqlQuery.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/GqlQuery.java similarity index 87% rename from src/main/java/com/google/gcloud/datastore/GqlQuery.java rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/GqlQuery.java index 8569bae8c674..60c22637da56 100644 --- a/src/main/java/com/google/gcloud/datastore/GqlQuery.java +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/GqlQuery.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 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.gcloud.datastore; import static com.google.common.base.Preconditions.checkNotNull; @@ -27,9 +43,10 @@ *

A usage example:

* *

When the type of the results is known the preferred usage would be: - *

 {@code
- *   Query query = GqlQuery.builder(Query.Type.FULL, "select * from kind").build();
- *   QueryResult results = datastore.run(query);
+ * 
{@code
+ *   Query query =
+ *       Query.gqlQueryBuilder(Query.ResultType.ENTITY, "select * from kind").build();
+ *   QueryResults results = datastore.run(query);
  *   while (results.hasNext()) {
  *     Entity entity = results.next();
  *     ...
@@ -37,11 +54,11 @@
  * } 
* *

When the type of the results is unknown you can use this approach: - *

 {@code
- *   Query query = GqlQuery.builder("select __key__ from kind").build();
- *   QueryResult results = datastore.run(query);
+ * 
{@code
+ *   Query query = Query.gqlQueryBuilder("select __key__ from kind").build();
+ *   QueryResults results = datastore.run(query);
  *   if (Key.class.isAssignableFrom(results.resultClass())) {
- *     QueryResult keys = (QueryResult) results;
+ *     QueryResults keys = (QueryResults) results;
  *     while (keys.hasNext()) {
  *       Key key = keys.next();
  *       ...
@@ -142,15 +159,15 @@ static Binding fromPb(DatastoreV1.GqlQueryArg argPb) {
    */
   public static final class Builder {
 
-    private final Type type;
+    private final ResultType resultType;
     private String namespace;
     private String queryString;
     private boolean allowLiteral;
-    private Map namedBindings = new TreeMap<>();
-    private List positionalBindings = new LinkedList<>();
+    private final Map namedBindings = new TreeMap<>();
+    private final List positionalBindings = new LinkedList<>();
 
-    Builder(Type type, String query) {
-      this.type = checkNotNull(type);
+    Builder(ResultType resultType, String query) {
+      this.resultType = checkNotNull(resultType);
       queryString = checkNotNull(query);
     }
 
@@ -210,7 +227,7 @@ public Builder setBinding(String name, Key... value) {
       return this;
     }
 
-    public Builder setBinding(String name, PartialEntity... value) {
+    public Builder setBinding(String name, FullEntity... value) {
       namedBindings.put(name, toBinding(name, EntityValue.MARSHALLER, Arrays.asList(value)));
       return this;
     }
@@ -255,7 +272,7 @@ public Builder addBinding(Key... value) {
       return this;
     }
 
-    public Builder addBinding(PartialEntity... value) {
+    public Builder addBinding(FullEntity... value) {
       positionalBindings.add(toBinding(EntityValue.MARSHALLER, Arrays.asList(value)));
       return this;
     }
@@ -294,7 +311,7 @@ private static Binding toBinding(String name, Value.BuilderFactory builderFactor
   }
 
   private GqlQuery(Builder builder) {
-    super(builder.type, builder.namespace);
+    super(builder.resultType, builder.namespace);
     queryString = builder.queryString;
     allowLiteral = builder.allowLiteral;
     namedBindings = ImmutableList.copyOf(builder.namedBindings.values());
@@ -373,20 +390,19 @@ protected void populatePb(DatastoreV1.RunQueryRequest.Builder requestPb) {
 
   @Override
   protected GqlQuery nextQuery(DatastoreV1.QueryResultBatch responsePb) {
-    // See b/18705483
+    // See issue #17
     throw new UnsupportedOperationException("paging for this query is not implemented yet");
   }
 
   @Override
-  protected Object fromPb(Type resultType, String namespace, byte[] bytesPb)
+  protected Object fromPb(ResultType resultType, String namespace, byte[] bytesPb)
       throws InvalidProtocolBufferException {
     return fromPb(resultType, namespace, DatastoreV1.GqlQuery.parseFrom(bytesPb));
   }
 
-  static  GqlQuery fromPb(Type resultType, String namespace,
-      DatastoreV1.GqlQuery queryPb) {
+  private static  GqlQuery fromPb(ResultType resultType, String ns, DatastoreV1.GqlQuery queryPb) {
     Builder builder = new Builder<>(resultType, queryPb.getQueryString());
-    builder.namespace(namespace);
+    builder.namespace(ns);
     if (queryPb.hasAllowLiteral()) {
       builder.allowLiteral = queryPb.getAllowLiteral();
     }
@@ -401,21 +417,4 @@ static  GqlQuery fromPb(Type resultType, String namespace,
     return builder.build();
   }
 
-  /**
-   * Returns a new GQL query builder.
-   *
-   * @see GQL Reference
-   */
-  public static GqlQuery.Builder builder(String gql) {
-    return builder(Type.UNKNOWN, gql);
-  }
-
-  /**
-   * Returns a new GQL query builder.
-   *
-   * @see GQL Reference
-   */
-  public static  GqlQuery.Builder builder(Type type, String gql) {
-    return new GqlQuery.Builder<>(type, gql);
-  }
 }
diff --git a/src/main/java/com/google/gcloud/datastore/PartialKey.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/IncompleteKey.java
similarity index 50%
rename from src/main/java/com/google/gcloud/datastore/PartialKey.java
rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/IncompleteKey.java
index e819aab8ec0b..6134eed2905b 100644
--- a/src/main/java/com/google/gcloud/datastore/PartialKey.java
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/IncompleteKey.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
 
 import com.google.api.services.datastore.DatastoreV1;
@@ -8,43 +24,33 @@
 import java.util.List;
 
 /**
- * A partial key (without a name or id).
- * Could be used as metadata for {@link PartialEntity}.
+ * An incomplete key (without a name or id).
  * This class is immutable.
  */
-public class PartialKey extends BaseKey {
+public class IncompleteKey extends BaseKey {
 
   private static final long serialVersionUID = -75301206578793347L;
 
   public static class Builder extends BaseKey.Builder {
 
-    private Builder(String dataset, String kind) {
-      super(dataset, kind);
+    private Builder(String projectId, String kind) {
+      super(projectId, kind);
     }
 
-    private Builder(PartialKey copyFrom) {
+    private Builder(IncompleteKey copyFrom) {
       super(copyFrom);
     }
 
     @Override
-    public PartialKey build() {
+    public IncompleteKey build() {
       ImmutableList path = ImmutableList.builder()
           .addAll(ancestors).add(PathElement.of(kind)).build();
-      return new PartialKey(dataset, namespace, path);
+      return new IncompleteKey(projectId, namespace, path);
     }
   }
 
-  PartialKey(String dataset, String namespace, ImmutableList path) {
-    super(dataset, namespace, path);
-  }
-
-  public Key newKey(String name) {
-    return Key.builder(dataset(), kind(), name)
-        .namespace(namespace()).ancestors(ancestors()).build();
-  }
-
-  public Key newKey(long id) {
-    return Key.builder(dataset(), kind(), id).namespace(namespace()).ancestors(ancestors()).build();
+  IncompleteKey(String projectId, String namespace, ImmutableList path) {
+    super(projectId, namespace, path);
   }
 
   @Override
@@ -52,20 +58,20 @@ protected Object fromPb(byte[] bytesPb) throws InvalidProtocolBufferException {
     return fromPb(DatastoreV1.Key.parseFrom(bytesPb));
   }
 
-  static PartialKey fromPb(DatastoreV1.Key keyPb) {
-    String dataset = null;
+  static IncompleteKey fromPb(DatastoreV1.Key keyPb) {
+    String projectId = null;
     String namespace = null;
     if (keyPb.hasPartitionId()) {
       DatastoreV1.PartitionId partitionIdPb = keyPb.getPartitionId();
       if (partitionIdPb.hasDatasetId()) {
-        dataset = partitionIdPb.getDatasetId();
+        projectId = partitionIdPb.getDatasetId();
       }
       if (partitionIdPb.hasNamespace()) {
         namespace = partitionIdPb.getNamespace();
       }
     }
     List pathElementsPb = keyPb.getPathElementList();
-    Preconditions.checkArgument(pathElementsPb.size() > 0, "Path must not be empty");
+    Preconditions.checkArgument(!pathElementsPb.isEmpty(), "Path must not be empty");
     ImmutableList.Builder pathBuilder = ImmutableList.builder();
     for (DatastoreV1.Key.PathElement pathElementPb : pathElementsPb) {
       pathBuilder.add(PathElement.fromPb(pathElementPb));
@@ -73,20 +79,20 @@ static PartialKey fromPb(DatastoreV1.Key keyPb) {
     ImmutableList path = pathBuilder.build();
     PathElement leaf = path.get(path.size() - 1);
     if (leaf.nameOrId() != null) {
-      return new Key(dataset, namespace, path);
+      return new Key(projectId, namespace, path);
     }
-    return new PartialKey(dataset, namespace, path);
+    return new IncompleteKey(projectId, namespace, path);
   }
 
-  public static Builder builder(String dataset, String kind) {
-    return new Builder(dataset, kind);
+  public static Builder builder(String projectId, String kind) {
+    return new Builder(projectId, kind);
   }
 
-  public static Builder builder(PartialKey copyFrom) {
+  public static Builder builder(IncompleteKey copyFrom) {
     return new Builder(copyFrom);
   }
 
   public static Builder builder(Key parent, String kind) {
-    return builder(parent.dataset(), kind).namespace(parent.namespace()).ancestors(parent.path());
+    return builder(parent.projectId(), kind).namespace(parent.namespace()).ancestors(parent.path());
   }
 }
diff --git a/src/main/java/com/google/gcloud/datastore/Key.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Key.java
similarity index 60%
rename from src/main/java/com/google/gcloud/datastore/Key.java
rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Key.java
index 113802a8ba5b..8b04898ffcd0 100644
--- a/src/main/java/com/google/gcloud/datastore/Key.java
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Key.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -5,8 +21,8 @@
 import com.google.api.services.datastore.DatastoreV1;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
-import com.google.protobuf.ByteString;
 import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.TextFormat;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
@@ -19,7 +35,7 @@
  *
  * @see Google Cloud Datastore Entities, Properties, and Keys
  */
-public final class Key extends PartialKey {
+public final class Key extends IncompleteKey {
 
   private static final long serialVersionUID = 3160994559785491356L;
 
@@ -28,13 +44,23 @@ public static final class Builder extends BaseKey.Builder {
     private String name;
     private Long id;
 
-    private Builder(String dataset, String kind, String name) {
-      super(dataset, kind);
+    private Builder(String projectId, String kind, String name) {
+      super(projectId, kind);
       this.name = name;
     }
 
-    private Builder(String dataset, String kind, long id) {
-      super(dataset, kind);
+    private Builder(String projectId, String kind, long id) {
+      super(projectId, kind);
+      this.id = id;
+    }
+
+    private Builder(IncompleteKey copyFrom, String name) {
+      super(copyFrom);
+      this.name = name;
+    }
+
+    private Builder(IncompleteKey copyFrom, long id) {
+      super(copyFrom);
       this.id = id;
     }
 
@@ -68,12 +94,12 @@ public Key build() {
       } else {
         pathBuilder.add(PathElement.of(kind, id));
       }
-      return new Key(dataset, namespace, pathBuilder.build());
+      return new Key(projectId, namespace, pathBuilder.build());
     }
   }
 
-  Key(String dataset, String namespace, ImmutableList path) {
-    super(dataset, namespace, path);
+  Key(String projectId, String namespace, ImmutableList path) {
+    super(projectId, namespace, path);
     Preconditions.checkArgument(nameOrId() != null);
   }
 
@@ -112,26 +138,27 @@ public Object nameOrId() {
    */
   public String toUrlSafe() {
     try {
-      return URLEncoder.encode(toString(), UTF_8.name());
+      return URLEncoder.encode(TextFormat.printToString(toPb()), UTF_8.name());
     } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("Unxpeced encoding exception", e);
+      throw new IllegalStateException("Unexpected encoding exception", e);
     }
   }
 
   /**
    * Create a {@code Key} given its URL safe encoded form.
    *
-   * @throws RuntimeException when decoding fails
+   * @throws IllegalArgumentException when decoding fails
    */
   public static Key fromUrlSafe(String urlSafe) {
     try {
       String utf8Str = URLDecoder.decode(urlSafe, UTF_8.name());
-      DatastoreV1.Key keyPb = DatastoreV1.Key.parseFrom(ByteString.copyFromUtf8(utf8Str));
-      return fromPb(keyPb);
+      DatastoreV1.Key.Builder builder = DatastoreV1.Key.newBuilder();
+      TextFormat.merge(utf8Str, builder);
+      return fromPb(builder.build());
     } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("Unxpeced decoding exception", e);
-    } catch (InvalidProtocolBufferException e) {
-      throw new RuntimeException("Could not parse key", e);
+      throw new IllegalStateException("Unexpected decoding exception", e);
+    } catch (TextFormat.ParseException e) {
+      throw new IllegalArgumentException("Could not parse key", e);
     }
   }
 
@@ -141,31 +168,39 @@ protected Object fromPb(byte[] bytesPb) throws InvalidProtocolBufferException {
   }
 
   static Key fromPb(DatastoreV1.Key keyPb) {
-    PartialKey key = PartialKey.fromPb(keyPb);
+    IncompleteKey key = IncompleteKey.fromPb(keyPb);
     Preconditions.checkState(key instanceof Key, "Key is not complete");
     return (Key) key;
   }
 
-  public static Builder builder(String dataset, String kind, String name) {
-    return new Builder(dataset, kind, name);
+  public static Builder builder(String projectId, String kind, String name) {
+    return new Builder(projectId, kind, name);
   }
 
-  public static Builder builder(String dataset, String kind, long id) {
-    return new Builder(dataset, kind, id);
+  public static Builder builder(String projectId, String kind, long id) {
+    return new Builder(projectId, kind, id);
   }
 
   public static Builder builder(Key copyFrom) {
     return new Builder(copyFrom);
   }
 
+  public static Builder builder(IncompleteKey copyFrom, String name) {
+    return new Builder(copyFrom, name);
+  }
+
+  public static Builder builder(IncompleteKey copyFrom, long id) {
+    return new Builder(copyFrom, id);
+  }
+
   public static Builder builder(Key parent, String kind, String name) {
-    Builder builder = builder(parent.dataset(), kind, name);
+    Builder builder = builder(parent.projectId(), kind, name);
     addParentToBuilder(parent, builder);
     return builder;
   }
 
   public static Builder builder(Key parent, String kind, long id) {
-    Builder builder = builder(parent.dataset(), kind, id);
+    Builder builder = builder(parent.projectId(), kind, id);
     addParentToBuilder(parent, builder);
     return builder;
   }
diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/KeyFactory.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/KeyFactory.java
new file mode 100644
index 000000000000..8010468c5068
--- /dev/null
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/KeyFactory.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * An helper for creating keys for a specific {@link Datastore},
+ * using its associated projectId and namespace.
+ */
+public final class KeyFactory extends BaseKey.Builder {
+
+  private final String pi;
+  private final String ns;
+
+  public KeyFactory(String projectId) {
+    this(projectId, null);
+  }
+
+  public KeyFactory(String projectId, String namespace) {
+    super(projectId);
+    namespace(namespace);
+    this.pi = projectId;
+    this.ns = namespace;
+  }
+
+  public IncompleteKey newKey() {
+    ImmutableList path = ImmutableList.builder()
+        .addAll(ancestors).add(PathElement.of(kind)).build();
+    return new IncompleteKey(projectId, namespace, path);
+  }
+
+  public Key newKey(String name) {
+    ImmutableList path = ImmutableList.builder()
+        .addAll(ancestors).add(PathElement.of(kind, name)).build();
+    return new Key(projectId, namespace, path);
+  }
+
+  public Key newKey(long id) {
+    ImmutableList path = ImmutableList.builder()
+        .addAll(ancestors).add(PathElement.of(kind, id)).build();
+    return new Key(projectId, namespace, path);
+  }
+
+  /**
+   * Resets the KeyFactory to its initial state.
+   * @return {@code this} for chaining.
+   */
+  public KeyFactory reset() {
+    projectId(pi);
+    namespace(ns);
+    kind = null;
+    ancestors.clear();
+    return this;
+  }
+
+  @Override
+  protected IncompleteKey build() {
+    return newKey();
+  }
+}
diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/KeyValue.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/KeyValue.java
new file mode 100644
index 000000000000..252f48ebc92a
--- /dev/null
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/KeyValue.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static com.google.api.services.datastore.DatastoreV1.Value.KEY_VALUE_FIELD_NUMBER;
+
+import com.google.api.services.datastore.DatastoreV1;
+
+public final class KeyValue extends Value {
+
+  private static final long serialVersionUID = -1318353707326704821L;
+
+  static final BaseMarshaller MARSHALLER =
+      new BaseMarshaller() {
+
+        private static final long serialVersionUID = 5449133205064700403L;
+
+        @Override
+        public int getProtoFieldId() {
+          return KEY_VALUE_FIELD_NUMBER;
+        }
+
+        @Override
+        public Builder newBuilder(Key key) {
+          return builder(key);
+        }
+
+        @Override
+        protected Key getValue(DatastoreV1.Value from) {
+          return Key.fromPb(from.getKeyValue());
+        }
+
+        @Override
+        protected void setValue(KeyValue from, DatastoreV1.Value.Builder to) {
+          to.setKeyValue(from.get().toPb());
+        }
+      };
+
+  public static final class Builder extends Value.BaseBuilder {
+
+    public Builder() {
+      super(ValueType.KEY);
+    }
+
+    @Override
+    public KeyValue build() {
+      return new KeyValue(this);
+    }
+  }
+
+  public KeyValue(Key key) {
+    this(builder(key));
+  }
+
+  private KeyValue(Builder builder) {
+    super(builder);
+  }
+
+  @Override
+  public Builder toBuilder() {
+    return new Builder().mergeFrom(this);
+  }
+
+  public static KeyValue of(Key key) {
+    return new KeyValue(key);
+  }
+
+  public static Builder builder(Key key) {
+    return new Builder().set(key);
+  }
+}
diff --git a/src/main/java/com/google/gcloud/datastore/ListValue.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ListValue.java
similarity index 56%
rename from src/main/java/com/google/gcloud/datastore/ListValue.java
rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ListValue.java
index 5af230d768b1..41c7e82788b5 100644
--- a/src/main/java/com/google/gcloud/datastore/ListValue.java
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ListValue.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
 
 import static com.google.api.services.datastore.DatastoreV1.Value.LIST_VALUE_FIELD_NUMBER;
@@ -16,32 +32,34 @@ public final class ListValue extends Value>> {
   static final BaseMarshaller>, ListValue, Builder> MARSHALLER =
       new BaseMarshaller>, ListValue, Builder>() {
 
-    @Override
-    public int getProtoFieldId() {
-      return LIST_VALUE_FIELD_NUMBER;
-    }
-
-    @Override
-    public Builder newBuilder(List> values) {
-      return builder().set(values);
-    }
-
-    @Override
-    protected List> getValue(DatastoreV1.Value from) {
-      List> properties = new ArrayList<>(from.getListValueCount());
-      for (DatastoreV1.Value valuePb : from.getListValueList()) {
-        properties.add(Value.fromPb(valuePb));
-      }
-      return properties;
-    }
-
-    @Override
-    protected void setValue(ListValue from, DatastoreV1.Value.Builder to) {
-      for (Value property : from.get()) {
-        to.addListValue(property.toPb());
-      }
-    }
-  };
+        private static final long serialVersionUID = -3193794036327640106L;
+
+        @Override
+        public int getProtoFieldId() {
+          return LIST_VALUE_FIELD_NUMBER;
+        }
+
+        @Override
+        public Builder newBuilder(List> values) {
+          return builder().set(values);
+        }
+
+        @Override
+        protected List> getValue(DatastoreV1.Value from) {
+          List> properties = new ArrayList<>(from.getListValueCount());
+          for (DatastoreV1.Value valuePb : from.getListValueList()) {
+            properties.add(Value.fromPb(valuePb));
+          }
+          return properties;
+        }
+
+        @Override
+        protected void setValue(ListValue from, DatastoreV1.Value.Builder to) {
+          for (Value property : from.get()) {
+            to.addListValue(property.toPb());
+          }
+        }
+      };
 
   public static final class Builder extends
       Value.BaseBuilder>, ListValue, Builder> {
@@ -49,11 +67,12 @@ public static final class Builder extends
     private ImmutableList.Builder> listBuilder = ImmutableList.builder();
 
     private Builder() {
-      super(Type.LIST);
+      super(ValueType.LIST);
     }
 
     public Builder addValue(Value value) {
-      Preconditions.checkArgument(value.type() != Type.LIST, "Cannot contain another list");
+      // see datastore_v1.proto definition for list_value
+      Preconditions.checkArgument(value.type() != ValueType.LIST, "Cannot contain another list");
       listBuilder.add(value);
       return this;
     }
@@ -68,9 +87,8 @@ public Builder addValue(Value first, Value... other) {
 
     @Override
     public Builder indexed(boolean indexed) {
-      // see b/18704917
-      DatastoreServiceException.throwInvalidRequest("ListValue can't specify index");
-      return this;
+      // see issue #26
+      throw DatastoreException.throwInvalidRequest("ListValue can't specify index");
     }
 
     /**
@@ -80,7 +98,7 @@ public Builder indexed(boolean indexed) {
      */
     @Override
     public Builder set(List> values) {
-      listBuilder = ImmutableList.>builder();
+      listBuilder = ImmutableList.builder();
       for (Value value : values) {
         addValue(value);
       }
diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/LongValue.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/LongValue.java
new file mode 100644
index 000000000000..43d139e90249
--- /dev/null
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/LongValue.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static com.google.api.services.datastore.DatastoreV1.Value.INTEGER_VALUE_FIELD_NUMBER;
+
+import com.google.api.services.datastore.DatastoreV1;
+
+public final class LongValue extends Value {
+
+  private static final long serialVersionUID = -8552854340400546861L;
+
+  static final BaseMarshaller MARSHALLER =
+      new BaseMarshaller() {
+
+        private static final long serialVersionUID = 2137414214660959845L;
+
+        @Override
+        public int getProtoFieldId() {
+          return INTEGER_VALUE_FIELD_NUMBER;
+        }
+
+        @Override
+        public Builder newBuilder(Long value) {
+          return builder(value);
+        }
+
+        @Override
+        protected Long getValue(DatastoreV1.Value from) {
+          return from.getIntegerValue();
+        }
+
+        @Override
+        protected void setValue(LongValue from, DatastoreV1.Value.Builder to) {
+          to.setIntegerValue(from.get());
+        }
+      };
+
+  public static final class Builder extends Value.BaseBuilder {
+
+    private Builder() {
+      super(ValueType.LONG);
+    }
+
+    @Override
+    public LongValue build() {
+      return new LongValue(this);
+    }
+  }
+
+  public LongValue(long value) {
+    this(builder(value));
+  }
+
+  private LongValue(Builder builder) {
+    super(builder);
+  }
+
+  @Override
+  public Builder toBuilder() {
+    return new Builder().mergeFrom(this);
+  }
+
+  public static LongValue of(long value) {
+    return new LongValue(value);
+  }
+
+  public static Builder builder(long value) {
+    return new Builder().set(value);
+  }
+}
diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/NullValue.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/NullValue.java
new file mode 100644
index 000000000000..58fed152ffd5
--- /dev/null
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/NullValue.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.api.services.datastore.DatastoreV1;
+
+public final class NullValue extends Value {
+
+  private static final long serialVersionUID = 8497300779013002270L;
+
+  static final BaseMarshaller MARSHALLER =
+      new BaseMarshaller() {
+
+        private static final long serialVersionUID = 2785573597627128832L;
+
+        @Override
+        public Builder newBuilder(Void value) {
+          return builder();
+        }
+
+        @Override
+        public int getProtoFieldId() {
+          return 0;
+        }
+
+        @Override
+        protected Void getValue(DatastoreV1.Value from) {
+          return null;
+        }
+
+        @Override
+        protected void setValue(NullValue from, DatastoreV1.Value.Builder to) {
+          // nothing to set
+        }
+      };
+
+  public static final class Builder extends Value.BaseBuilder {
+
+    private Builder() {
+      super(ValueType.NULL);
+    }
+
+    @Override
+    public NullValue build() {
+      return new NullValue(this);
+    }
+
+    @Override
+    public Builder set(Void value) {
+      checkArgument(value == null, "Only null values are allowed");
+      return this;
+    }
+  }
+
+  public NullValue() {
+    this(builder());
+  }
+
+  private NullValue(Builder builder) {
+    super(builder);
+  }
+
+  @Override
+  public Builder toBuilder() {
+    return new Builder().mergeFrom(this);
+  }
+
+  public static NullValue of() {
+    return new NullValue();
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+}
diff --git a/src/main/java/com/google/gcloud/datastore/PathElement.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/PathElement.java
similarity index 78%
rename from src/main/java/com/google/gcloud/datastore/PathElement.java
rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/PathElement.java
index 9e84764e55f7..186ed97adcde 100644
--- a/src/main/java/com/google/gcloud/datastore/PathElement.java
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/PathElement.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
 
 import static com.google.common.base.Preconditions.checkArgument;
@@ -89,11 +105,12 @@ protected Object fromPb(byte[] bytesPb) throws InvalidProtocolBufferException {
   static PathElement fromPb(DatastoreV1.Key.PathElement pathElementPb) {
     String kind = pathElementPb.getKind();
     if (pathElementPb.hasId()) {
-      return PathElement.of(kind, pathElementPb.getId());
-    } else if (pathElementPb.hasName()) {
-      return PathElement.of(kind, pathElementPb.getName());
+      return of(kind, pathElementPb.getId());
+    }
+    if (pathElementPb.hasName()) {
+      return of(kind, pathElementPb.getName());
     }
-    return PathElement.of(kind);
+    return of(kind);
   }
 
   static PathElement of(String kind) {
diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ProjectionEntity.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ProjectionEntity.java
new file mode 100644
index 000000000000..acd9783fe836
--- /dev/null
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ProjectionEntity.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import com.google.api.services.datastore.DatastoreV1;
+import com.google.protobuf.ByteString;
+
+/**
+ * A projection entity is a result of a Google Cloud Datastore projection query.
+ * A projection entity holds one or more properties, represented by a name (as {@link String})
+ * and a value (as {@link Value}), and may have a {@link Key}.
+ *
+ * @see Google Cloud Datastore projection queries
+ * @see Google Cloud Datastore Entities, Properties, and Keys
+ */
+public final class ProjectionEntity extends BaseEntity {
+
+  private static final long serialVersionUID = 432961565733066915L;
+
+  static final class Builder extends BaseEntity.Builder {
+
+    Builder() {
+    }
+
+    private Builder(ProjectionEntity entity) {
+      super(entity);
+    }
+
+    @Override
+    public ProjectionEntity build() {
+      return new ProjectionEntity(this);
+    }
+  }
+
+  ProjectionEntity(Builder builder) {
+    super(builder);
+  }
+
+  @SuppressWarnings({"unchecked", "deprecation"})
+  @Override
+  public DateTime getDateTime(String name) {
+    Value value = getValue(name);
+    if (value.hasMeaning() && value.meaning() == 18 && value instanceof LongValue) {
+      return new DateTime(getLong(name));
+    }
+    return ((Value) value).get();
+  }
+
+  @SuppressWarnings({"unchecked", "deprecation"})
+  @Override
+  public Blob getBlob(String name) {
+    Value value = getValue(name);
+    if (value.hasMeaning() && value.meaning() == 18 && value instanceof StringValue) {
+      return new Blob(ByteString.copyFromUtf8(getString(name)));
+    }
+    return ((Value) value).get();
+  }
+
+  static ProjectionEntity fromPb(DatastoreV1.Entity entityPb) {
+    return new Builder().fill(entityPb).build();
+  }
+
+  @Override
+  protected Builder emptyBuilder() {
+    return new Builder();
+  }
+
+  public static Builder builder(ProjectionEntity copyFrom) {
+    return new Builder(copyFrom);
+  }
+}
diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Query.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Query.java
new file mode 100644
index 000000000000..093dc7283327
--- /dev/null
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Query.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.services.datastore.DatastoreV1;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.collect.Maps;
+import com.google.gcloud.datastore.StructuredQuery.EntityQueryBuilder;
+import com.google.gcloud.datastore.StructuredQuery.KeyQueryBuilder;
+import com.google.gcloud.datastore.StructuredQuery.ProjectionEntityQueryBuilder;
+import com.google.protobuf.GeneratedMessage;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.util.Map;
+
+
+/**
+ * A Google Cloud Datastore query.
+ * For usage examples see {@link GqlQuery} and {@link StructuredQuery}.
+ *
+ * @param  the type of the values returned by this query.
+ * @see Datastore Queries
+ */
+public abstract class Query extends Serializable {
+
+  private static final long serialVersionUID = -2748141759901313101L;
+
+  private final ResultType resultType;
+  private final String namespace;
+
+  /**
+   * This class represents the expected type of the result.
+   *   ENTITY: A full entity represented by {@link Entity}.
+   *   PROJECTION_ENTITY: A projection entity, represented by {@link ProjectionEntity}.
+   *   KEY: An entity's {@link Key}.
+   */
+  public abstract static class ResultType implements java.io.Serializable {
+
+    private static final long serialVersionUID = 2104157695425806623L;
+    private static final Map>
+        PB_TO_INSTANCE = Maps.newEnumMap(DatastoreV1.EntityResult.ResultType.class);
+
+    static final ResultType UNKNOWN = new ResultType(null, Object.class) {
+
+      private static final long serialVersionUID = 1602329532153860907L;
+
+      @Override protected Object convert(DatastoreV1.Entity entityPb) {
+        if (entityPb.getPropertyCount() == 0) {
+          if (!entityPb.hasKey()) {
+            return null;
+          }
+          return Key.fromPb(entityPb.getKey());
+        }
+        return ProjectionEntity.fromPb(entityPb);
+      }
+    };
+
+    public static final ResultType ENTITY =
+        new ResultType(DatastoreV1.EntityResult.ResultType.FULL, Entity.class) {
+
+      private static final long serialVersionUID = 7712959777507168274L;
+
+      @Override protected Entity convert(DatastoreV1.Entity entityPb) {
+        return Entity.fromPb(entityPb);
+      }
+    };
+
+    public static final ResultType KEY =
+        new ResultType(DatastoreV1.EntityResult.ResultType.KEY_ONLY, Key.class) {
+
+      private static final long serialVersionUID = -8514289244104446252L;
+
+      @Override protected Key convert(DatastoreV1.Entity entityPb) {
+        return Key.fromPb(entityPb.getKey());
+      }
+    };
+
+    public static final ResultType PROJECTION_ENTITY =
+        new ResultType(DatastoreV1.EntityResult.ResultType.PROJECTION,
+            ProjectionEntity.class) {
+
+          private static final long serialVersionUID = -7591409419690650246L;
+
+          @Override protected ProjectionEntity convert(DatastoreV1.Entity entityPb) {
+            return ProjectionEntity.fromPb(entityPb);
+          }
+    };
+
+    private final Class resultClass;
+    private final DatastoreV1.EntityResult.ResultType queryType;
+
+    @SuppressWarnings("unchecked")
+    private ResultType(DatastoreV1.EntityResult.ResultType queryType, Class resultClass) {
+      this.queryType = queryType;
+      this.resultClass = resultClass;
+      if (queryType != null) {
+        PB_TO_INSTANCE.put(queryType, this);
+      }
+    }
+
+    public Class resultClass() {
+      return resultClass;
+    }
+
+    @Override
+    public int hashCode() {
+      return resultClass.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) {
+        return true;
+      }
+      if (!(obj instanceof ResultType)) {
+        return false;
+      }
+      ResultType other = (ResultType) obj;
+      return resultClass.equals(other.resultClass);
+    }
+
+    @Override
+    public String toString() {
+      ToStringHelper toStringHelper = MoreObjects.toStringHelper(this);
+      toStringHelper.add("queryType", queryType);
+      toStringHelper.add("resultClass", resultClass);
+      return toStringHelper.toString();
+    }
+
+    boolean isAssignableFrom(ResultType otherResultType) {
+      return resultClass.isAssignableFrom(otherResultType.resultClass);
+    }
+
+    protected abstract V convert(DatastoreV1.Entity entityPb);
+
+    static ResultType fromPb(DatastoreV1.EntityResult.ResultType typePb) {
+      return MoreObjects.firstNonNull(PB_TO_INSTANCE.get(typePb), UNKNOWN);
+    }
+  }
+
+  Query(ResultType resultType, String namespace) {
+    this.resultType = checkNotNull(resultType);
+    this.namespace = namespace;
+  }
+
+  ResultType type() {
+    return resultType;
+  }
+
+  public String namespace() {
+    return namespace;
+  }
+
+  @Override
+  public String toString() {
+    ToStringHelper toStringHelper = MoreObjects.toStringHelper(this);
+    toStringHelper.add("type", resultType);
+    toStringHelper.add("namespace", namespace);
+    toStringHelper.add("queryPb", super.toString());
+    return toStringHelper.toString();
+  }
+
+  @Override
+  protected Object fromPb(byte[] bytesPb) throws InvalidProtocolBufferException {
+    return fromPb(resultType, namespace, bytesPb);
+  }
+
+  protected abstract Object fromPb(ResultType resultType, String namespace, byte[] bytesPb)
+      throws InvalidProtocolBufferException;
+
+  protected abstract void populatePb(DatastoreV1.RunQueryRequest.Builder requestPb);
+
+  protected abstract Query nextQuery(DatastoreV1.QueryResultBatch responsePb);
+
+  /**
+   * Returns a new {@link GqlQuery} builder.
+   *
+   * @see GQL Reference
+   */
+  public static GqlQuery.Builder gqlQueryBuilder(String gql) {
+    return gqlQueryBuilder(ResultType.UNKNOWN, gql);
+  }
+
+  /**
+   * Returns a new {@link GqlQuery} builder.
+   *
+   * @see GQL Reference
+   */
+  public static  GqlQuery.Builder gqlQueryBuilder(ResultType resultType, String gql) {
+    return new GqlQuery.Builder<>(resultType, gql);
+  }
+
+  /**
+   * Returns a new {@link StructuredQuery} builder for full (complete entities) queries.
+   */
+  public static EntityQueryBuilder entityQueryBuilder() {
+    return new EntityQueryBuilder();
+  }
+
+  /**
+   * Returns a new {@link StructuredQuery} builder for key only queries.
+   */
+  public static KeyQueryBuilder keyQueryBuilder() {
+    return new KeyQueryBuilder();
+  }
+
+  /**
+   * Returns a new {@link StructuredQuery} builder for projection queries.
+   */
+  public static ProjectionEntityQueryBuilder projectionEntityQueryBuilder() {
+    return new ProjectionEntityQueryBuilder();
+  }
+}
diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/QueryResults.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/QueryResults.java
new file mode 100644
index 000000000000..44360987b573
--- /dev/null
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/QueryResults.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import java.util.Iterator;
+
+/**
+ * The result of a Google Cloud Datastore query submission.
+ * When result is not typed it is possible to cast it to its appropriate type according to
+ * the {@link #resultClass} value.
+ * Results are loaded lazily therefore it is possible to get a {@code DatastoreException}
+ * upon {@link Iterator#hasNext hasNext} or {@link Iterator#next next} calls.
+ *
+ * @param  the type of the results value.
+ */
+public interface QueryResults extends Iterator {
+
+  /**
+   * Returns the actual class of the result's values.
+   */
+  Class resultClass();
+
+  /**
+   * Returns the Cursor for point after the value returned in the last {@link #next} call.
+   * Not currently implemented (depends on v1beta3).
+   */
+  Cursor cursorAfter();
+}
diff --git a/src/main/java/com/google/gcloud/datastore/QueryResultImpl.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/QueryResultsImpl.java
similarity index 56%
rename from src/main/java/com/google/gcloud/datastore/QueryResultImpl.java
rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/QueryResultsImpl.java
index 5c32f028b6b3..8e2f294ed15d 100644
--- a/src/main/java/com/google/gcloud/datastore/QueryResultImpl.java
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/QueryResultsImpl.java
@@ -1,35 +1,52 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
 
 import com.google.api.services.datastore.DatastoreV1;
 import com.google.api.services.datastore.DatastoreV1.QueryResultBatch.MoreResultsType;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.AbstractIterator;
-import com.google.gcloud.datastore.Query.Type;
+import com.google.gcloud.datastore.Query.ResultType;
 
 import java.util.Iterator;
+import java.util.Objects;
 
-class QueryResultImpl extends AbstractIterator implements QueryResult {
+class QueryResultsImpl extends AbstractIterator implements QueryResults {
 
-  private final DatastoreServiceImpl datastore;
+  private final DatastoreImpl datastore;
   private final DatastoreV1.ReadOptions readOptionsPb;
   private final DatastoreV1.PartitionId partitionIdPb;
-  private final Query.Type queryType;
+  private final ResultType queryResultType;
   private Query query;
-  private Query.Type actualType;
+  private ResultType actualResultType;
   private DatastoreV1.QueryResultBatch queryResultBatchPb;
   private boolean lastBatch;
   private Iterator entityResultPbIter;
   //private ByteString cursor; // only available in v1beta3
 
 
-  QueryResultImpl(DatastoreServiceImpl datastore, DatastoreV1.ReadOptions readOptionsPb,
-      Query query) {
+  QueryResultsImpl(DatastoreImpl datastore, DatastoreV1.ReadOptions readOptionsPb,
+                   Query query) {
     this.datastore = datastore;
     this.readOptionsPb = readOptionsPb;
     this.query = query;
-    queryType = query.type();
+    queryResultType = query.type();
     DatastoreV1.PartitionId.Builder pbBuilder = DatastoreV1.PartitionId.newBuilder();
-    pbBuilder.setDatasetId(datastore.options().dataset());
+    pbBuilder.setDatasetId(datastore.options().projectId());
     if (query.namespace() != null) {
       pbBuilder.setNamespace(query.namespace());
     } else if (datastore.options().namespace() != null) {
@@ -49,14 +66,14 @@ private void sendRequest() {
     queryResultBatchPb = datastore.runQuery(requestPb.build()).getBatch();
     lastBatch = queryResultBatchPb.getMoreResults() != MoreResultsType.NOT_FINISHED;
     entityResultPbIter = queryResultBatchPb.getEntityResultList().iterator();
-    // cursor = resultPb.getSkippedCursor(); // only available in v1beta3
-    actualType = Type.fromPb(queryResultBatchPb.getEntityResultType());
-    if (queryType == Type.PROJECTION) {
+    // cursor = resultPb.getSkippedCursor(); // available in v1beta3, use startCursor if not skipped
+    actualResultType = ResultType.fromPb(queryResultBatchPb.getEntityResultType());
+    if (Objects.equals(queryResultType, ResultType.PROJECTION_ENTITY)) {
       // projection entity can represent all type of results
-      actualType = Type.PROJECTION;
+      actualResultType = ResultType.PROJECTION_ENTITY;
     }
-    Preconditions.checkState(queryType.isAssignableFrom(actualType),
-        "Unexpected result type " + actualType + " vs " + queryType);
+    Preconditions.checkState(queryResultType.isAssignableFrom(actualResultType),
+        "Unexpected result type " + actualResultType + " vs " + queryResultType);
   }
 
   @SuppressWarnings("unchecked")
@@ -71,16 +88,16 @@ protected T computeNext() {
     }
     DatastoreV1.EntityResult entityResultPb = entityResultPbIter.next();
     //cursor = entityResultPb.getCursor(); // only available in v1beta3
-    return (T) actualType.convert(entityResultPb.getEntity());
+    return (T) actualResultType.convert(entityResultPb.getEntity());
   }
 
   @Override
   public Class resultClass() {
-    return actualType.resultClass();
+    return actualResultType.resultClass();
   }
 
   @Override
-  public Cursor cursor() {
+  public Cursor cursorAfter() {
     //return new Cursor(cursor); // only available in v1beta3
     return null;
   }
diff --git a/src/main/java/com/google/gcloud/datastore/RawValue.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/RawValue.java
similarity index 50%
rename from src/main/java/com/google/gcloud/datastore/RawValue.java
rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/RawValue.java
index d5fc455df4a9..550d27804eba 100644
--- a/src/main/java/com/google/gcloud/datastore/RawValue.java
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/RawValue.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
 
 import com.google.api.services.datastore.DatastoreV1;
@@ -9,31 +25,33 @@ public final class RawValue extends Value {
   static final BaseMarshaller MARSHALLER =
       new BaseMarshaller() {
 
-    @Override
-    public Builder newBuilder(DatastoreV1.Value value) {
-      return builder(value);
-    }
+        private static final long serialVersionUID = 5320642719486106244L;
 
-    @Override
-    public int getProtoFieldId() {
-      return 0;
-    }
+        @Override
+        public Builder newBuilder(DatastoreV1.Value value) {
+          return builder(value);
+        }
 
-    @Override
-    protected DatastoreV1.Value getValue(DatastoreV1.Value from) {
-      return from;
-    }
+        @Override
+        public int getProtoFieldId() {
+          return 0;
+        }
 
-    @Override
-    protected void setValue(RawValue from, DatastoreV1.Value.Builder to) {
-      to.mergeFrom(from.get());
-    }
-  };
+        @Override
+        protected DatastoreV1.Value getValue(DatastoreV1.Value from) {
+          return from;
+        }
+
+        @Override
+        protected void setValue(RawValue from, DatastoreV1.Value.Builder to) {
+          to.mergeFrom(from.get());
+        }
+      };
 
   static final class Builder extends Value.BaseBuilder {
 
     private Builder() {
-      super(Type.RAW_VALUE);
+      super(ValueType.RAW_VALUE);
     }
 
     @Override
diff --git a/src/main/java/com/google/gcloud/datastore/Serializable.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Serializable.java
similarity index 53%
rename from src/main/java/com/google/gcloud/datastore/Serializable.java
rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Serializable.java
index c9f6b7984512..ff62fe89195f 100644
--- a/src/main/java/com/google/gcloud/datastore/Serializable.java
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Serializable.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
 
 import com.google.protobuf.GeneratedMessage;
@@ -20,14 +36,14 @@ public String toString() {
     return toPb().toString();
   }
 
-  private void writeObject(ObjectOutputStream out) throws IOException {
-    out.defaultWriteObject();
-    out.writeObject(toPb().toByteArray());
+  private void writeObject(ObjectOutputStream output) throws IOException {
+    output.defaultWriteObject();
+    output.writeObject(toPb().toByteArray());
   }
 
-  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
-    in.defaultReadObject();
-    bytesPb = (byte[]) in.readObject();
+  private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
+    input.defaultReadObject();
+    bytesPb = (byte[]) input.readObject();
   }
 
   protected Object readResolve() throws ObjectStreamException {
diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/StringValue.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/StringValue.java
new file mode 100644
index 000000000000..95a31e714876
--- /dev/null
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/StringValue.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static com.google.api.services.datastore.DatastoreV1.Value.STRING_VALUE_FIELD_NUMBER;
+
+import com.google.api.services.datastore.DatastoreV1;
+
+public final class StringValue extends Value {
+
+  private static final long serialVersionUID = -3105699707394545523L;
+
+  static final BaseMarshaller MARSHALLER =
+      new BaseMarshaller() {
+
+        private static final long serialVersionUID = -359610204134164436L;
+
+        @Override
+        public int getProtoFieldId() {
+          return STRING_VALUE_FIELD_NUMBER;
+        }
+
+        @Override
+        public Builder newBuilder(String value) {
+          return builder(value);
+        }
+
+        @Override
+        protected String getValue(DatastoreV1.Value from) {
+          return from.getStringValue();
+        }
+
+        @Override
+        protected void setValue(StringValue from, DatastoreV1.Value.Builder to) {
+          to.setStringValue(from.get());
+        }
+      };
+
+  public static final class Builder extends Value.BaseBuilder {
+
+    private Builder() {
+      super(ValueType.STRING);
+    }
+
+    @Override
+    public StringValue build() {
+      return new StringValue(this);
+    }
+  }
+
+  public StringValue(String value) {
+    this(builder(value));
+  }
+
+  private StringValue(Builder builder) {
+    super(builder);
+  }
+
+  @Override
+  public Builder toBuilder() {
+    return new Builder().mergeFrom(this);
+  }
+
+  public static StringValue of(String value) {
+    return new StringValue(value);
+  }
+
+  public static Builder builder(String value) {
+    return new Builder().set(value);
+  }
+}
diff --git a/src/main/java/com/google/gcloud/datastore/StructuredQuery.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/StructuredQuery.java
similarity index 85%
rename from src/main/java/com/google/gcloud/datastore/StructuredQuery.java
rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/StructuredQuery.java
index 84d7d65e2c25..cb141b6218b0 100644
--- a/src/main/java/com/google/gcloud/datastore/StructuredQuery.java
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/StructuredQuery.java
@@ -1,6 +1,22 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
 
-import static com.google.api.client.util.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gcloud.datastore.BlobValue.of;
 import static com.google.gcloud.datastore.BooleanValue.of;
 import static com.google.gcloud.datastore.DateTimeValue.of;
@@ -18,6 +34,7 @@
 
 import java.io.Serializable;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
@@ -29,19 +46,26 @@
  * 

A usage example:

* *

A simple query that returns all entities for a specific kind - *

 {@code
- *   StructuredQuery query = StructuredQuery.builder().kind(kind).build();
- *   QueryResult results = datastore.run(query);
+ * 
{@code
+ *   Query query = Query.entityQueryBuilder().kind(kind).build();
+ *   QueryResults results = datastore.run(query);
  *   while (results.hasNext()) {
  *     Entity entity = results.next();
  *     ...
  *   }
  * } 
* + *

A simple key-only query of all entities for a specific kind + *

{@code
+ *   Query keyOnlyQuery =  Query.keyQueryBuilder().kind(KIND1).build();
+ *   QueryResults results = datastore.run(keyOnlyQuery);
+ *   ...
+ * } 
+ * *

A less trivial example of a projection query that returns the first 10 results * of "age" and "name" properties (sorted and grouped by "age") with an age greater than 18 - *

 {@code
- *   StructuredQuery query = StructuredQuery.projectionBuilder()
+ * 
{@code
+ *   Query query = Query.projectionEntityQueryBuilder()
  *       .kind(kind)
  *       .projection(Projection.property("age"), Projection.first("name"))
  *       .filter(PropertyFilter.gt("age", 18))
@@ -49,7 +73,7 @@
  *       .orderBy(OrderBy.asc("age"))
  *       .limit(10)
  *       .build();
- *   QueryResult results = datastore.run(query);
+ *   QueryResults results = datastore.run(query);
  *   ...
  * } 
* @@ -140,7 +164,7 @@ public boolean equals(Object obj) { return false; } CompositeFilter other = (CompositeFilter) obj; - return operator.equals(other.operator) + return operator == other.operator && filters.equals(other.filters); } @@ -231,7 +255,7 @@ public boolean equals(Object obj) { } PropertyFilter other = (PropertyFilter) obj; return property.equals(other.property) - && operator.equals(other.operator) + && operator == other.operator && Objects.equals(value, other.value); } @@ -456,7 +480,7 @@ public boolean equals(Object obj) { } OrderBy other = (OrderBy) obj; return property.equals(other.property) - && direction.equals(other.direction); + && direction == other.direction; } public String property() { @@ -577,24 +601,24 @@ public static Projection first(String property) { static class BaseBuilder> { - private Type type; + private final ResultType resultType; private String namespace; private String kind; - private List projection = new LinkedList<>(); + private final List projection = new LinkedList<>(); private Filter filter; - private List groupBy = new LinkedList<>(); - private List orderBy = new LinkedList<>(); + private final List groupBy = new LinkedList<>(); + private final List orderBy = new LinkedList<>(); private Cursor startCursor; private Cursor endCursor; private int offset; private Integer limit; - BaseBuilder(Type type) { - this.type = type; + BaseBuilder(ResultType resultType) { + this.resultType = resultType; } @SuppressWarnings("unchecked") - protected B self() { + B self() { return (B) this; } @@ -648,51 +672,45 @@ public B orderBy(OrderBy orderBy, OrderBy... others) { public B addOrderBy(OrderBy orderBy, OrderBy... others) { this.orderBy.add(orderBy); - for (OrderBy other : others) { - this.orderBy.add(other); - } + Collections.addAll(this.orderBy, others); return self(); } - protected B clearProjection() { + B clearProjection() { projection.clear(); return self(); } - protected B projection(Projection projection, Projection... others) { + B projection(Projection projection, Projection... others) { clearProjection(); addProjection(projection, others); return self(); } - protected B addProjection(Projection projection, Projection... others) { + B addProjection(Projection projection, Projection... others) { this.projection.add(projection); - for (Projection other : others) { - this.projection.add(other); - } + Collections.addAll(this.projection, others); return self(); } - protected B clearGroupBy() { + B clearGroupBy() { groupBy.clear(); return self(); } - protected B groupBy(String property, String... others) { + B groupBy(String property, String... others) { clearGroupBy(); addGroupBy(property, others); return self(); } - protected B addGroupBy(String property, String... others) { + B addGroupBy(String property, String... others) { this.groupBy.add(property); - for (String other : others) { - this.groupBy.add(other); - } + Collections.addAll(this.groupBy, others); return self(); } - protected B mergeFrom(DatastoreV1.Query queryPb) { + B mergeFrom(DatastoreV1.Query queryPb) { if (queryPb.getKindCount() > 0) { kind(queryPb.getKind(0).getName()); } @@ -728,22 +746,34 @@ public StructuredQuery build() { } } - public static final class Builder extends BaseBuilder> { + static final class Builder extends BaseBuilder> { - public Builder(Type type) { - super(type); + Builder(ResultType resultType) { + super(resultType); } } - public static final class KeyOnlyBuilder extends BaseBuilder { + public static final class EntityQueryBuilder extends BaseBuilder { - public KeyOnlyBuilder() { - super(Type.KEY_ONLY); + EntityQueryBuilder() { + super(ResultType.ENTITY); + } + + @Override + public StructuredQuery build() { + return new StructuredQuery<>(this); + } + } + + public static final class KeyQueryBuilder extends BaseBuilder { + + KeyQueryBuilder() { + super(ResultType.KEY); projection(Projection.property(KEY_PROPERTY_NAME)); } @Override - protected KeyOnlyBuilder mergeFrom(DatastoreV1.Query queryPb) { + protected KeyQueryBuilder mergeFrom(DatastoreV1.Query queryPb) { super.mergeFrom(queryPb); projection(Projection.property(KEY_PROPERTY_NAME)); clearGroupBy(); @@ -751,84 +781,56 @@ protected KeyOnlyBuilder mergeFrom(DatastoreV1.Query queryPb) { } @Override - public KeyOnlyQuery build() { - return new KeyOnlyQuery(this); + public StructuredQuery build() { + return new StructuredQuery<>(this); } } - public static final class ProjectionBuilder - extends BaseBuilder { + public static final class ProjectionEntityQueryBuilder + extends BaseBuilder { - public ProjectionBuilder() { - super(Type.PROJECTION); + ProjectionEntityQueryBuilder() { + super(ResultType.PROJECTION_ENTITY); } @Override - public ProjectionQuery build() { - return new ProjectionQuery(this); + public StructuredQuery build() { + return new StructuredQuery<>(this); } @Override - public ProjectionBuilder clearProjection() { + public ProjectionEntityQueryBuilder clearProjection() { return super.clearProjection(); } @Override - public ProjectionBuilder projection(Projection projection, Projection... others) { + public ProjectionEntityQueryBuilder projection(Projection projection, Projection... others) { return super.projection(projection, others); } @Override - public ProjectionBuilder addProjection(Projection projection, Projection... others) { + public ProjectionEntityQueryBuilder addProjection(Projection projection, Projection... others) { return super.addProjection(projection, others); } @Override - public ProjectionBuilder clearGroupBy() { + public ProjectionEntityQueryBuilder clearGroupBy() { return super.clearGroupBy(); } @Override - public ProjectionBuilder groupBy(String property, String... others) { + public ProjectionEntityQueryBuilder groupBy(String property, String... others) { return super.groupBy(property, others); } @Override - public ProjectionBuilder addGroupBy(String property, String... others) { + public ProjectionEntityQueryBuilder addGroupBy(String property, String... others) { return super.addGroupBy(property, others); } } - public static final class KeyOnlyQuery extends StructuredQuery { - - private static final long serialVersionUID = -7502917784216095473L; - - KeyOnlyQuery(KeyOnlyBuilder builder) { - super(builder); - } - } - - public static final class ProjectionQuery extends StructuredQuery { - - private static final long serialVersionUID = -3333183044486150649L; - - ProjectionQuery(ProjectionBuilder builder) { - super(builder); - } - - @Override - public List projection() { - return super.projection(); - } - - @Override - public List groupBy() { - return super.groupBy(); - } - } - StructuredQuery(BaseBuilder builder) { - super(builder.type, builder.namespace); + super(builder.resultType, builder.namespace); kind = builder.kind; projection = ImmutableList.copyOf(builder.projection); filter = builder.filter; @@ -872,11 +874,11 @@ public String kind() { return kind; } - protected boolean keyOnly() { - return projection.size() == 1 && projection.get(0).property.equals(KEY_PROPERTY_NAME); + boolean keyOnly() { + return projection.size() == 1 && KEY_PROPERTY_NAME.equals(projection.get(0).property); } - protected List projection() { + public List projection() { return projection; } @@ -884,7 +886,7 @@ public Filter filter() { return filter; } - protected List groupBy() { + public List groupBy() { return groupBy; } @@ -963,32 +965,21 @@ protected DatastoreV1.Query toPb() { } @Override - protected Object fromPb(Type type, String namespace, byte[] bytesPb) + protected Object fromPb(ResultType resultType, String namespace, byte[] bytesPb) throws InvalidProtocolBufferException { - return fromPb(type, namespace, DatastoreV1.Query.parseFrom(bytesPb)); + return fromPb(resultType, namespace, DatastoreV1.Query.parseFrom(bytesPb)); } - static StructuredQuery fromPb(Type type, String namespace, DatastoreV1.Query queryPb) { + private static StructuredQuery fromPb(ResultType resultType, String namespace, + DatastoreV1.Query queryPb) { BaseBuilder builder; - if (type.equals(Type.FULL)) { - builder = builder(); - } else if (type.equals(Type.KEY_ONLY)) { - builder = keyOnlyBuilder(); + if (resultType.equals(ResultType.ENTITY)) { + builder = new EntityQueryBuilder(); + } else if (resultType.equals(ResultType.KEY)) { + builder = new KeyQueryBuilder(); } else { - builder = projectionBuilder(); + builder = new ProjectionEntityQueryBuilder(); } return builder.namespace(namespace).mergeFrom(queryPb).build(); } - - public static Builder builder() { - return new Builder<>(Type.FULL); - } - - public static KeyOnlyBuilder keyOnlyBuilder() { - return new KeyOnlyBuilder(); - } - - public static ProjectionBuilder projectionBuilder() { - return new ProjectionBuilder(); - } } diff --git a/src/main/java/com/google/gcloud/datastore/Transaction.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Transaction.java similarity index 57% rename from src/main/java/com/google/gcloud/datastore/Transaction.java rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Transaction.java index 06c4e5a45997..9d676bc68a8c 100644 --- a/src/main/java/com/google/gcloud/datastore/Transaction.java +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Transaction.java @@ -1,10 +1,27 @@ +/* + * Copyright 2015 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.gcloud.datastore; import java.util.Iterator; +import java.util.List; /** * A Google cloud datastore transaction. - * Any write operation that is applied on a transaction will only be sent + * Similar to {@link Batch} any write operation that is applied on a transaction will only be sent * to the Datastore upon {@link #commit}. A call to {@link #rollback} will invalidate * the transaction and discard the changes. Any read operation that is done by a transaction * will be part of it and therefore a {@code commit} is guaranteed to fail if an entity @@ -33,7 +50,11 @@ * @see Google Cloud Datastore transactions * */ -public interface Transaction extends DatastoreReaderWriter { +public interface Transaction extends DatastoreBatchWriter, DatastoreReaderWriter { + + interface Response { + List generatedKeys(); + } /** * {@inheritDoc} @@ -41,7 +62,7 @@ public interface Transaction extends DatastoreReaderWriter { * to fail if entity was changed by others after it was seen by this transaction) but any * write changes in this transaction will not be reflected by the returned entity. * - * @throws DatastoreServiceException upon failure or if no longer active + * @throws DatastoreException upon failure or if no longer active */ @Override Entity get(Key key); @@ -52,10 +73,21 @@ public interface Transaction extends DatastoreReaderWriter { * to fail if any of the entities was changed by others after they were seen by this transaction) * but any write changes in this transaction will not be reflected by the returned entities. * - * @throws DatastoreServiceException upon failure or if no longer active + * @throws DatastoreException upon failure or if no longer active */ @Override - Iterator get(Key key, Key... others); + Iterator get(Key... key); + + /** + * {@inheritDoc} + * The requested entities will be part of this Datastore transaction (so a commit is guaranteed + * to fail if any of the entities was changed by others after they were seen by this transaction) + * but any write changes in this transaction will not be reflected by the returned entities. + * + * @throws DatastoreException upon failure or if no longer active + */ + @Override + List fetch(Key... keys); /** * {@inheritDoc} @@ -64,27 +96,33 @@ public interface Transaction extends DatastoreReaderWriter { * query was performed) but any write changes in this transaction will not be reflected by * the result. * - * @throws DatastoreServiceException upon failure or if no longer active + * @throws DatastoreException upon failure or if no longer active */ @Override - QueryResult run(Query query); + QueryResults run(Query query); /** * Commit the transaction. * - * @throws DatastoreServiceException if could not commit the transaction or if no longer active + * @throws DatastoreException if could not commit the transaction or if no longer active */ - void commit(); + Response commit(); /** * Rollback the transaction. * - * @throws DatastoreServiceException if transaction was already committed + * @throws DatastoreException if transaction was already committed */ void rollback(); /** * Returns {@code true} if the transaction is still active (was not committed or rolledback). */ + @Override boolean active(); + + /** + * Returns the transaction associated {@link Datastore}. + */ + Datastore datastore(); } diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/TransactionImpl.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/TransactionImpl.java new file mode 100644 index 000000000000..48568650910d --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/TransactionImpl.java @@ -0,0 +1,130 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import com.google.api.services.datastore.DatastoreV1; +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import com.google.gcloud.datastore.TransactionOption.ForceWrites; +import com.google.gcloud.datastore.TransactionOption.IsolationLevel; +import com.google.protobuf.ByteString; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +final class TransactionImpl extends BaseDatastoreBatchWriter implements Transaction { + + private final DatastoreImpl datastore; + private final ByteString transaction; + private final boolean force; + private boolean rolledback; + + static class ResponseImpl implements Transaction.Response { + + private final DatastoreV1.CommitResponse response; + + public ResponseImpl(DatastoreV1.CommitResponse response) { + this.response = response; + } + + @Override + public List generatedKeys() { + return Lists.transform(response.getMutationResult().getInsertAutoIdKeyList(), + new Function() { + @Override public Key apply(DatastoreV1.Key keyPb) { + return Key.fromPb(keyPb); + } + }); + } + } + + TransactionImpl(DatastoreImpl datastore, TransactionOption... options) { + super("transaction"); + this.datastore = datastore; + DatastoreV1.BeginTransactionRequest.Builder requestPb = + DatastoreV1.BeginTransactionRequest.newBuilder(); + Map, TransactionOption> optionsMap = + TransactionOption.asImmutableMap(options); + IsolationLevel isolationLevel = (IsolationLevel) optionsMap.get(IsolationLevel.class); + if (isolationLevel != null) { + requestPb.setIsolationLevel(isolationLevel.level().toPb()); + } + ForceWrites forceWrites = (ForceWrites) optionsMap.get(TransactionOption.ForceWrites.class); + force = forceWrites != null && forceWrites.force(); + transaction = datastore.requestTransactionId(requestPb); + } + + @Override + public Entity get(Key key) { + return DatastoreHelper.get(this, key); + } + + @Override + public Iterator get(Key... keys) { + validateActive(); + DatastoreV1.ReadOptions.Builder readOptionsPb = DatastoreV1.ReadOptions.newBuilder(); + readOptionsPb.setTransaction(transaction); + return datastore.get(readOptionsPb.build(), keys); + } + + @Override + public List fetch(Key... keys) { + validateActive(); + return DatastoreHelper.fetch(this, keys); + } + + @Override + public QueryResults run(Query query) { + validateActive(); + DatastoreV1.ReadOptions.Builder readOptionsPb = DatastoreV1.ReadOptions.newBuilder(); + readOptionsPb.setTransaction(transaction); + return datastore.run(readOptionsPb.build(), query); + } + + @Override + public Transaction.Response commit() { + validateActive(); + DatastoreV1.Mutation.Builder mutationPb = toMutationPb(); + if (force) { + mutationPb.setForce(force); + } + DatastoreV1.CommitRequest.Builder requestPb = DatastoreV1.CommitRequest.newBuilder(); + requestPb.setMode(DatastoreV1.CommitRequest.Mode.TRANSACTIONAL); + requestPb.setTransaction(transaction); + requestPb.setMutation(mutationPb); + DatastoreV1.CommitResponse responsePb = datastore.commit(requestPb.build()); + deactivate(); + return new ResponseImpl(responsePb); + } + + @Override + public void rollback() { + if (rolledback) { + return; + } + validateActive(); + datastore.rollbackTransaction(transaction); + deactivate(); + rolledback = true; + } + + @Override + public Datastore datastore() { + return datastore; + } +} diff --git a/src/main/java/com/google/gcloud/datastore/TransactionOption.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/TransactionOption.java similarity index 72% rename from src/main/java/com/google/gcloud/datastore/TransactionOption.java rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/TransactionOption.java index 597715df20a8..c1c8368213dc 100644 --- a/src/main/java/com/google/gcloud/datastore/TransactionOption.java +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/TransactionOption.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 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.gcloud.datastore; import com.google.api.services.datastore.DatastoreV1; @@ -26,8 +42,8 @@ public boolean force() { } @Override - BatchWriteOption toBatchWriteOption() { - return new BatchWriteOption.ForceWrites(force); + BatchOption toBatchWriteOption() { + return new BatchOption.ForceWrites(force); } } @@ -61,6 +77,11 @@ public IsolationLevel(Level level) { public Level level() { return level; } + + @Override + BatchOption toBatchWriteOption() { + return null; + } } TransactionOption() { @@ -89,7 +110,5 @@ static Map, TransactionOption> asImmutableMap return builder.build(); } - BatchWriteOption toBatchWriteOption() { - return null; - } + abstract BatchOption toBatchWriteOption(); } diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Validator.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Validator.java new file mode 100644 index 000000000000..09a3fa7defcc --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Validator.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; + +import java.util.regex.Pattern; + +/** + * Utility to validate Datastore type/values. + */ +final class Validator { + + private static final Pattern PROJECT_ID_PATTERN = Pattern.compile( + "([a-z\\d\\-]{1,100}~)?([a-z\\d][a-z\\d\\-\\.]{0,99}:)?([a-z\\d][a-z\\d\\-]{0,99})"); + private static final int MAX_NAMESPACE_LENGTH = 100; + private static final Pattern NAMESPACE_PATTERN = + Pattern.compile(String.format("[0-9A-Za-z\\._\\-]{0,%d}", MAX_NAMESPACE_LENGTH)); + + private Validator() { + // utility class + } + + static String validateDatabase(String projectId) { + checkArgument(!Strings.isNullOrEmpty(projectId), "projectId can't be empty or null"); + checkArgument(PROJECT_ID_PATTERN.matcher(projectId).matches(), + "projectId must match the following pattern: " + PROJECT_ID_PATTERN.pattern()); + return projectId; + } + + static String validateNamespace(String namespace) { + if (namespace != null) { + checkArgument(!namespace.isEmpty(), "namespace must not be an empty string"); + checkArgument(namespace.length() <= MAX_NAMESPACE_LENGTH, + "namespace must not contain more than 100 characters"); + checkArgument(NAMESPACE_PATTERN.matcher(namespace).matches(), + "namespace must the following pattern: " + NAMESPACE_PATTERN.pattern()); + } + return namespace; + } + + static String validateKind(String kind) { + checkArgument(!Strings.isNullOrEmpty(kind), "kind must not be empty or null"); + return kind; + } + + +} diff --git a/src/main/java/com/google/gcloud/datastore/Value.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Value.java similarity index 55% rename from src/main/java/com/google/gcloud/datastore/Value.java rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Value.java index 4686624adf54..c5fc63a960b1 100644 --- a/src/main/java/com/google/gcloud/datastore/Value.java +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/Value.java @@ -1,14 +1,27 @@ +/* + * Copyright 2015 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.gcloud.datastore; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.services.datastore.DatastoreV1; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.InvalidProtocolBufferException; -import java.util.HashMap; -import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -23,128 +36,24 @@ public abstract class Value extends Serializable { private static final long serialVersionUID = -1899638277588872742L; - private static final Map DESCRIPTOR_TO_TYPE_MAP = new HashMap<>(); - private final transient Type type; + + private final transient ValueType valueType; private final transient Boolean indexed; private final transient Integer meaning; private final transient V value; - /** - * The type of a property. - * - * @see Google Cloud Datastore types - */ - public enum Type { - - /** - * Represents a {@code null} value. - */ - NULL(NullValue.MARSHALLER), - - /** - * Represents a {@code string} value. - */ - STRING(StringValue.MARSHALLER), - - /** - * Represents an entity ({@link PartialEntity} or {@link Entity}) value. - */ - ENTITY(EntityValue.MARSHALLER), - - /** - * Represents a {@code list} of {@link Value}s. - */ - LIST(ListValue.MARSHALLER), - - /** - * Represents a {@code key} as a value. - */ - KEY(KeyValue.MARSHALLER), - - /** - * Represents a {@code long} value. - */ - LONG(LongValue.MARSHALLER), - - /** - * Represents a {@code double} value. - */ - DOUBLE(DoubleValue.MARSHALLER), - - /** - * Represents a {@code boolean} value. - */ - BOOLEAN(BooleanValue.MARSHALLER), - - /** - * Represents a {@link DateTime} value. - */ - DATE_TIME(DateTimeValue.MARSHALLER), - - /** - * Represents a {@link Blob} value. - */ - BLOB(BlobValue.MARSHALLER), - - /** - * Represents a raw/unparsed value. - */ - RAW_VALUE(RawValue.MARSHALLER); - - - @SuppressWarnings("rawtypes") private final Marshaller marshaller; - - , B extends Builder> Type(Marshaller marshaller) { - this.marshaller = marshaller; - int fieldId = marshaller.getProtoFieldId(); - if (fieldId > 0) { - DESCRIPTOR_TO_TYPE_MAP.put(fieldId, this); - } - } - - Marshaller getMarshaller() { - return marshaller; - } - } - - interface Builder, B extends Builder> { - - Type getType(); - - B mergeFrom(P other); - - Boolean getIndexed(); - - B indexed(boolean indexed); - - Integer getMeaning(); - - B meaning(Integer meaning); - - V get(); - - B set(V value); - - P build(); - } - - interface BuilderFactory, B extends Builder> { + interface BuilderFactory, B extends ValueBuilder> + extends java.io.Serializable { B newBuilder(V value); } - interface Marshaller, B extends Builder> { + abstract static class BaseMarshaller, B extends ValueBuilder> + implements ValueMarshaller, BuilderFactory { - B fromProto(DatastoreV1.Value proto); - - DatastoreV1.Value toProto(P value); - - int getProtoFieldId(); - } - - abstract static class BaseMarshaller, B extends Builder> - implements Marshaller, BuilderFactory { + private static final long serialVersionUID = 2880767488942992985L; + @SuppressWarnings("deprecation") @Override public final B fromProto(DatastoreV1.Value proto) { B builder = newBuilder(getValue(proto)); @@ -157,6 +66,7 @@ public final B fromProto(DatastoreV1.Value proto) { return builder; } + @SuppressWarnings("deprecation") @Override public final DatastoreV1.Value toProto(P value) { DatastoreV1.Value.Builder builder = DatastoreV1.Value.newBuilder(); @@ -175,21 +85,22 @@ public final DatastoreV1.Value toProto(P value) { protected abstract void setValue(P from, DatastoreV1.Value.Builder to); } + @SuppressWarnings("deprecation") abstract static class BaseBuilder, B extends BaseBuilder> - implements Builder { + implements ValueBuilder { - private final Type type; + private final ValueType valueType; private Boolean indexed; private Integer meaning; private V value; - protected BaseBuilder(Type type) { - this.type = type; + BaseBuilder(ValueType valueType) { + this.valueType = valueType; } @Override - public Type getType() { - return type; + public ValueType getValueType() { + return valueType; } @Override @@ -242,23 +153,15 @@ private B self() { public abstract P build(); } -

, B extends BaseBuilder> Value(Builder builder) { - type = builder.getType(); +

, B extends BaseBuilder> Value(ValueBuilder builder) { + valueType = builder.getValueType(); indexed = builder.getIndexed(); meaning = builder.getMeaning(); - // some validations: - if (meaning != null && indexed != null) { - // TODO: consider supplying Ranges for allowed meaning and validating it here - // more specific validation could be done on the specific types themselves - // upon construction [e.g. integer with a meaning 13 should be in the range [0,100]] - checkArgument(!indexed || meaning != 15 && meaning != 22, - "Indexed values should not have meaning with 15 or 22"); - } value = builder.get(); } - public final Type type() { - return type; + public final ValueType type() { + return valueType; } public final boolean hasIndexed() { @@ -269,10 +172,12 @@ public final Boolean indexed() { return indexed; } + @Deprecated public final boolean hasMeaning() { return meaning != null; } + @Deprecated public final Integer meaning() { return meaning; } @@ -281,11 +186,11 @@ public final V get() { return value; } - public abstract Builder toBuilder(); + public abstract ValueBuilder toBuilder(); @Override public int hashCode() { - return Objects.hash(type, indexed, meaning, value); + return Objects.hash(valueType, indexed, meaning, value); } @Override @@ -298,7 +203,7 @@ public boolean equals(Object obj) { return false; } Value other = (Value) obj; - return Objects.equals(type, other.type) + return Objects.equals(valueType, other.valueType) && Objects.equals(indexed, other.indexed) && Objects.equals(meaning, other.meaning) && Objects.equals(value, other.value); @@ -314,12 +219,12 @@ static Value fromPb(DatastoreV1.Value proto) { for (Entry entry : proto.getAllFields().entrySet()) { FieldDescriptor descriptor = entry.getKey(); if (descriptor.getName().endsWith("_value")) { - Type type = DESCRIPTOR_TO_TYPE_MAP.get(descriptor.getNumber()); - if (type == null) { + ValueType valueType = ValueType.getByDescriptorId(descriptor.getNumber()); + if (valueType == null) { // unsupported type return RawValue.MARSHALLER.fromProto(proto).build(); } - return type.getMarshaller().fromProto(proto).build(); + return valueType.getMarshaller().fromProto(proto).build(); } } return NullValue.MARSHALLER.fromProto(proto).build(); diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ValueBuilder.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ValueBuilder.java new file mode 100644 index 000000000000..99a44f19366a --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ValueBuilder.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +/** + * A common interface for Value builders. + */ +interface ValueBuilder, B extends ValueBuilder> { + + ValueType getValueType(); + + B mergeFrom(P other); + + Boolean getIndexed(); + + B indexed(boolean indexed); + + Integer getMeaning(); + + @Deprecated + B meaning(Integer meaning); + + V get(); + + B set(V value); + + P build(); +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ValueMarshaller.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ValueMarshaller.java new file mode 100644 index 000000000000..da1a8c77fc03 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ValueMarshaller.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import com.google.api.services.datastore.DatastoreV1; + +/** + * A common interface for Value marshallers. + */ +interface ValueMarshaller, B extends ValueBuilder> + extends java.io.Serializable { + + B fromProto(DatastoreV1.Value proto); + + DatastoreV1.Value toProto(P value); + + int getProtoFieldId(); +} diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ValueType.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ValueType.java new file mode 100644 index 000000000000..5b515d6a0901 --- /dev/null +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/ValueType.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015 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.gcloud.datastore; + +import com.google.common.collect.ImmutableMap; + +/** + * The type of a Datastore property. + * + * @see Google Cloud Datastore types + */ +public enum ValueType { + + /** + * Represents a {@code null} value. + */ + NULL(NullValue.MARSHALLER), + + /** + * Represents a {@code string} value. + */ + STRING(StringValue.MARSHALLER), + + /** + * Represents an entity value. + */ + ENTITY(EntityValue.MARSHALLER), + + /** + * Represents a {@code list} of {@link Value}s. + */ + LIST(ListValue.MARSHALLER), + + /** + * Represents a {@code key} as a value. + */ + KEY(KeyValue.MARSHALLER), + + /** + * Represents a {@code long} value. + */ + LONG(LongValue.MARSHALLER), + + /** + * Represents a {@code double} value. + */ + DOUBLE(DoubleValue.MARSHALLER), + + /** + * Represents a {@code boolean} value. + */ + BOOLEAN(BooleanValue.MARSHALLER), + + /** + * Represents a {@link DateTime} value. + */ + DATE_TIME(DateTimeValue.MARSHALLER), + + /** + * Represents a {@link Blob} value. + */ + BLOB(BlobValue.MARSHALLER), + + /** + * Represents a raw/unparsed value. + */ + RAW_VALUE(RawValue.MARSHALLER); + + + private static final ImmutableMap DESCRIPTOR_TO_TYPE_MAP; + + @SuppressWarnings("rawtypes") private final ValueMarshaller marshaller; + + static { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (ValueType valueType : ValueType.values()) { + int fieldId = valueType.getMarshaller().getProtoFieldId(); + if (fieldId > 0) { + builder.put(fieldId, valueType); + } + } + DESCRIPTOR_TO_TYPE_MAP = builder.build(); + } + + + , B extends ValueBuilder> ValueType(ValueMarshaller marshaller) { + this.marshaller = marshaller; + } + + ValueMarshaller getMarshaller() { + return marshaller; + } + + static ValueType getByDescriptorId(int descriptorId) { + return DESCRIPTOR_TO_TYPE_MAP.get(descriptorId); + } +} diff --git a/src/main/java/com/google/gcloud/datastore/package-info.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/package-info.java similarity index 54% rename from src/main/java/com/google/gcloud/datastore/package-info.java rename to gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/package-info.java index 88933a598ca9..3b402820e663 100644 --- a/src/main/java/com/google/gcloud/datastore/package-info.java +++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/package-info.java @@ -1,11 +1,27 @@ +/* + * Copyright 2015 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. + */ + /** * A client to the Google Cloud Datastore. * *

A simple usage example: *

 {@code
- * DatastoreServiceOptions options = DatastoreServiceOptions.builder().dataset(DATASET).build();
- * DatastoreService datastore = DatastoreServiceFactory.getDefault(options);
- * KeyFactory keyFactory = new KeyFactory(datastore).kind(kind);
+ * DatastoreOptions options = DatastoreOptions.builder().projectId(PROJECT_ID).build();
+ * Datastore datastore = DatastoreFactory.instance().get(options);
+ * KeyFactory keyFactory = datastore.newKeyFactory().kind(kind);
  * Key key = keyFactory.newKey(keyName);
  * Entity entity = datastore.get(key);
  * if (entity == null) {
diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/spi/DatastoreRpc.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/spi/DatastoreRpc.java
new file mode 100644
index 000000000000..be194c7b3848
--- /dev/null
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/spi/DatastoreRpc.java
@@ -0,0 +1 @@
+/*
 * Copyright 2015 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.gcloud.spi;

import com.google.api.services.datastore.DatastoreV1.AllocateIdsRequest;
import com.google.api.services.datastore.DatastoreV1.AllocateIdsResponse;
import com.google.api.services.datastore.DatastoreV1.BeginTransactionRequest;
import com.google.api.services.datastore.DatastoreV1.BeginTransactionResponse;
import com.google.api.services.datastore.DatastoreV1.CommitRequest;
import com.google.api.services.datastore.DatastoreV1.CommitResponse;
import com.google.api.services.datastore.DatastoreV1.LookupRequest;
import com.google.api.services.datastore.DatastoreV1.LookupResponse;
import com.google.api.services.datastore.DatastoreV1.RollbackRequest;
import com.google.api.services.datastore.DatastoreV1.RollbackResponse;
import com.google.api.services.datastore.DatastoreV1.RunQueryRequest;
import com.google.api.services.datastore.DatastoreV1.RunQueryResponse;

/**
 * Provides access to the remote Datastore service.
 */
public interface DatastoreRpc {

  public class DatastoreRpcException extends Exception {

    /**
     * The reason for the exception.
     *
     * @see Google Cloud Datastore error codes
     */
    public enum Reason {

      ABORTED(true, "Request aborted", 409),
      DEADLINE_EXCEEDED(true, "Deadline exceeded", 403),
      FAILED_PRECONDITION(false, "Invalid request", 412),
      INTERNAL(false, "Server returned an error", 500),
      INVALID_ARGUMENT(false, "Request parameter has an invalid value", 400),
      PERMISSION_DENIED(false, "Unauthorized request", 403),
      RESOURCE_EXHAUSTED(false, "Quota exceeded", 402),
      UNAVAILABLE(true, "Could not reach service", 503);

      private final boolean retryable;
      private final String description;
      private final int httpStatus;

      private Reason(boolean retryable, String description, int httpStatus) {
        this.retryable = retryable;
        this.description = description;
        this.httpStatus = httpStatus;
      }

      public boolean retryable() {
        return retryable;
      }

      public String description() {
        return description;
      }

      public int httpStatus() {
        return httpStatus;
      }
    }

    private final String reason;
    private final int httpStatus;
    private final boolean retryable;

    public DatastoreRpcException(Reason reason) {
      this(reason.name(), reason.httpStatus, reason.retryable, reason.description);
    }

    public DatastoreRpcException(String reason, int httpStatus, boolean retryable, String message) {
      super(message);
      this.reason = reason;
      this.httpStatus = httpStatus;
      this.retryable = retryable;
    }

    public String reason() {
      return reason;
    }

    public int httpStatus() {
      return httpStatus;
    }

    public boolean retryable() {
      return retryable;
    }
  }

  AllocateIdsResponse allocateIds(AllocateIdsRequest request) throws DatastoreRpcException;

  BeginTransactionResponse beginTransaction(BeginTransactionRequest request)
      throws DatastoreRpcException;

  CommitResponse commit(CommitRequest request) throws DatastoreRpcException;

  LookupResponse lookup(LookupRequest request) throws DatastoreRpcException;

  RollbackResponse rollback(RollbackRequest request) throws DatastoreRpcException;

  RunQueryResponse runQuery(RunQueryRequest request) throws DatastoreRpcException;
}
\ No newline at end of file
diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/spi/DatastoreRpcFactory.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/spi/DatastoreRpcFactory.java
new file mode 100644
index 000000000000..1815dda30f5d
--- /dev/null
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/spi/DatastoreRpcFactory.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015 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.gcloud.spi;
+
+import com.google.gcloud.datastore.DatastoreOptions;
+
+/**
+ * An interface for Datastore RPC factory.
+ * Implementation will be loaded via {@link java.util.ServiceLoader}.
+ */
+public interface DatastoreRpcFactory extends
+    ServiceRpcFactory {
+}
+
diff --git a/gcloud-java-datastore/src/main/java/com/google/gcloud/spi/DefaultDatastoreRpc.java b/gcloud-java-datastore/src/main/java/com/google/gcloud/spi/DefaultDatastoreRpc.java
new file mode 100644
index 000000000000..2f245260b325
--- /dev/null
+++ b/gcloud-java-datastore/src/main/java/com/google/gcloud/spi/DefaultDatastoreRpc.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2015 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.gcloud.spi;
+
+import com.google.api.services.datastore.DatastoreV1.AllocateIdsRequest;
+import com.google.api.services.datastore.DatastoreV1.AllocateIdsResponse;
+import com.google.api.services.datastore.DatastoreV1.BeginTransactionRequest;
+import com.google.api.services.datastore.DatastoreV1.BeginTransactionResponse;
+import com.google.api.services.datastore.DatastoreV1.CommitRequest;
+import com.google.api.services.datastore.DatastoreV1.CommitResponse;
+import com.google.api.services.datastore.DatastoreV1.LookupRequest;
+import com.google.api.services.datastore.DatastoreV1.LookupResponse;
+import com.google.api.services.datastore.DatastoreV1.RollbackRequest;
+import com.google.api.services.datastore.DatastoreV1.RollbackResponse;
+import com.google.api.services.datastore.DatastoreV1.RunQueryRequest;
+import com.google.api.services.datastore.DatastoreV1.RunQueryResponse;
+import com.google.api.services.datastore.client.Datastore;
+import com.google.api.services.datastore.client.DatastoreException;
+import com.google.api.services.datastore.client.DatastoreFactory;
+import com.google.api.services.datastore.client.DatastoreOptions.Builder;
+import com.google.common.collect.ImmutableMap;
+import com.google.gcloud.datastore.DatastoreOptions;
+import com.google.gcloud.spi.DatastoreRpc.DatastoreRpcException.Reason;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class DefaultDatastoreRpc implements DatastoreRpc {
+
+  private final Datastore client;
+
+  private static final ImmutableMap STR_TO_REASON;
+  private static final ImmutableMap HTTP_STATUS_TO_REASON;
+
+  static {
+    ImmutableMap.Builder builder = ImmutableMap.builder();
+    Map httpCodes = new HashMap<>();
+    for (Reason reason : Reason.values()) {
+      builder.put(reason.name(), reason);
+      httpCodes.put(reason.httpStatus(), reason);
+    }
+    STR_TO_REASON = builder.build();
+    HTTP_STATUS_TO_REASON = ImmutableMap.copyOf(httpCodes);
+  }
+
+  public DefaultDatastoreRpc(DatastoreOptions options) {
+    client = DatastoreFactory.get().create(
+        new Builder()
+            .dataset(options.projectId())
+            .host(options.host())
+            .initializer(options.httpRequestInitializer())
+            .build());
+  }
+
+  private static DatastoreRpcException translate(DatastoreException exception) {
+    String message = exception.getMessage();
+    String reasonStr = "";
+    if (message != null) {
+      try {
+        JSONObject json = new JSONObject(new JSONTokener(message));
+        JSONObject error = json.getJSONObject("error").getJSONArray("errors").getJSONObject(0);
+        reasonStr = error.getString("reason");
+        message = error.getString("message");
+      } catch (JSONException ignore) {
+        // ignore - will be converted to unknown
+      }
+    }
+    Reason reason = STR_TO_REASON.get(reasonStr);
+    if (reason == null) {
+      reason = HTTP_STATUS_TO_REASON.get(exception.getCode());
+    }
+    return reason != null
+        ? new DatastoreRpcException(reason)
+        : new DatastoreRpcException("Unknown", exception.getCode(), false, message);
+  }
+
+  @Override
+  public AllocateIdsResponse allocateIds(AllocateIdsRequest request)
+      throws DatastoreRpcException {
+    try {
+      return client.allocateIds(request);
+    } catch (DatastoreException ex) {
+      throw translate(ex);
+    }
+  }
+
+  @Override
+  public BeginTransactionResponse beginTransaction(BeginTransactionRequest request)
+      throws DatastoreRpcException {
+    try {
+      return client.beginTransaction(request);
+    } catch (DatastoreException ex) {
+      throw translate(ex);
+    }
+  }
+
+  @Override
+  public CommitResponse commit(CommitRequest request) throws DatastoreRpcException {
+    try {
+      return client.commit(request);
+    } catch (DatastoreException ex) {
+      throw translate(ex);
+    }
+  }
+
+  @Override
+  public LookupResponse lookup(LookupRequest request) throws DatastoreRpcException {
+    try {
+      return client.lookup(request);
+    } catch (DatastoreException ex) {
+      throw translate(ex);
+    }
+  }
+
+  @Override
+  public RollbackResponse rollback(RollbackRequest request) throws DatastoreRpcException {
+    try {
+      return client.rollback(request);
+    } catch (DatastoreException ex) {
+      throw translate(ex);
+    }
+  }
+
+  @Override
+  public RunQueryResponse runQuery(RunQueryRequest request) throws DatastoreRpcException {
+    try {
+      return client.runQuery(request);
+    } catch (DatastoreException ex) {
+      throw translate(ex);
+    }
+  }
+}
+
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BaseDatastoreBatchWriterTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BaseDatastoreBatchWriterTest.java
new file mode 100644
index 000000000000..c3f1bfbd5a71
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BaseDatastoreBatchWriterTest.java
@@ -0,0 +1 @@
+/*
 * Copyright 2015 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.gcloud.datastore;

import static org.easymock.EasyMock.*;
import static org.junit.Assert.assertEquals;

import com.google.api.services.datastore.DatastoreV1;
import com.google.common.collect.ImmutableList;
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.util.List;

public class BaseDatastoreBatchWriterTest {

  private static final Key KEY1 = Key.builder("dataset1", "kind1", "name1").build();
  private static final Key KEY2 = Key.builder(KEY1, 1).build();
  private static final Key KEY3 = Key.builder(KEY1, 2).build();
  private static final IncompleteKey INCOMPLETE_KEY = IncompleteKey.builder(KEY1).build();
  private static final Entity ENTITY1 = Entity.builder(KEY1).build();
  private static final Entity ENTITY2 = Entity.builder(KEY2).set("bak", true).build();
  private static final Entity ENTITY3 = Entity.builder(KEY3).set("bak", true).build();
  private static final FullEntity INCOMPLETE_ENTITY_1 =
      Entity.builder(INCOMPLETE_KEY).build();
  private static final FullEntity INCOMPLETE_ENTITY_2 =
      Entity.builder(INCOMPLETE_KEY).set("name", "dan").build();

  private DatastoreBatchWriter batchWriter;

  private class DatastoreBatchWriter extends BaseDatastoreBatchWriter {

    private final Datastore datastore;

    protected DatastoreBatchWriter() {
      super("test");
      datastore = EasyMock.createMock(Datastore.class);
      IncompleteKey[] expected = {INCOMPLETE_KEY, INCOMPLETE_KEY};
      List result = ImmutableList.of(KEY2, KEY3);
      expect(datastore.allocateId(expected)).andReturn(result).times(0, 1);
      replay(datastore);
    }

    @Override
    protected Datastore datastore() {
      return datastore;
    }

    void finish() {
      verify(datastore);
    }
  }

  @Before
  public void setUp() {
    batchWriter = new DatastoreBatchWriter();
  }

  @After
  public void tearDown() {
    batchWriter.finish();
  }

  @Test
  public void testAdd() throws Exception {
    Entity entity2 =
        Entity.builder(ENTITY2).key(Key.builder(KEY1).name("name2").build()).build();
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addInsert(ENTITY1.toPb())
        .addInsert(entity2.toPb())
        .addInsert(Entity.builder(KEY2, INCOMPLETE_ENTITY_1).build().toPb())
        .addInsert(Entity.builder(KEY3, INCOMPLETE_ENTITY_2).build().toPb())
        .build();
    List entities = batchWriter
        .add(ENTITY1, INCOMPLETE_ENTITY_1, INCOMPLETE_ENTITY_2, entity2);
    assertEquals(pb, batchWriter.toMutationPb().build());
    assertEquals(ENTITY1, entities.get(0));
    assertEquals(Entity.builder(KEY2, INCOMPLETE_ENTITY_1).build(), entities.get(1));
    assertEquals(Entity.builder(KEY3, INCOMPLETE_ENTITY_2).build(), entities.get(2));
    assertEquals(entity2, entities.get(3));
  }

  @Test
  public void testAddAfterDelete() throws Exception {
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addUpsert(ENTITY1.toPb())
        .build();
    batchWriter.delete(KEY1);
    batchWriter.add(ENTITY1);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test(expected = DatastoreException.class)
  public void testAddDuplicate() throws Exception {
    batchWriter.add(ENTITY1);
    batchWriter.add(ENTITY1);
  }

  @Test(expected = DatastoreException.class)
  public void testAddAfterPut() throws Exception {
    batchWriter.put(ENTITY1);
    batchWriter.add(ENTITY1);
  }

  @Test(expected = DatastoreException.class)
  public void testAddAfterUpdate() throws Exception {
    batchWriter.update(ENTITY1);
    batchWriter.add(ENTITY1);
  }

  @Test(expected = DatastoreException.class)
  public void testAddWhenNotActive() throws Exception {
    batchWriter.deactivate();
    batchWriter.add(ENTITY1);
  }

  @Test
  public void testAddWithDeferredAllocation() throws Exception {
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addInsert(ENTITY1.toPb())
        .addInsertAutoId(INCOMPLETE_ENTITY_1.toPb())
        .addInsertAutoId(INCOMPLETE_ENTITY_2.toPb())
        .build();
    batchWriter.addWithDeferredIdAllocation(ENTITY1, INCOMPLETE_ENTITY_1);
    batchWriter.addWithDeferredIdAllocation(INCOMPLETE_ENTITY_2);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test(expected = DatastoreException.class)
  public void testAddWithDeferredAllocationWhenNotActive() throws Exception {
    batchWriter.deactivate();
    batchWriter.addWithDeferredIdAllocation(INCOMPLETE_ENTITY_1);
  }

  @Test
  public void testUpdate() throws Exception {
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addUpdate(ENTITY1.toPb())
        .addUpdate(ENTITY2.toPb())
        .addUpdate(ENTITY3.toPb())
        .build();
    batchWriter.update(ENTITY1, ENTITY2);
    batchWriter.update(ENTITY3);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test
  public void testUpdateAfterUpdate() throws Exception {
    Entity entity = Entity.builder(ENTITY1).set("foo", "bar").build();
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addUpdate(entity.toPb())
        .build();
    batchWriter.update(ENTITY1);
    batchWriter.update(entity);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test
  public void testUpdateAfterAdd() throws Exception {
    Entity entity = Entity.builder(ENTITY1).set("foo", "bar").build();
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addUpsert(entity.toPb())
        .build();
    batchWriter.add(ENTITY1);
    batchWriter.update(entity);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test
  public void testUpdateAfterPut() throws Exception {
    Entity entity = Entity.builder(ENTITY1).set("foo", "bar").build();
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addUpsert(entity.toPb())
        .build();
    batchWriter.put(ENTITY1);
    batchWriter.update(entity);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test(expected = DatastoreException.class)
  public void testUpdateAfterDelete() throws Exception {
    batchWriter.delete(KEY1);
    batchWriter.update(ENTITY1, ENTITY2);
  }

  @Test(expected = DatastoreException.class)
  public void testUpdateWhenNotActive() throws Exception {
    batchWriter.deactivate();
    batchWriter.update(ENTITY1);
  }

  @Test
  public void testPut() throws Exception {
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addUpsert(ENTITY1.toPb())
        .addUpsert(ENTITY2.toPb())
        .addUpsert(ENTITY3.toPb())
        .build();
    batchWriter.put(ENTITY1, ENTITY2);
    batchWriter.put(ENTITY3);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test
  public void testPutAfterPut() throws Exception {
    Entity entity = Entity.builder(ENTITY1).set("foo", "bar").build();
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addUpsert(entity.toPb())
        .build();
    batchWriter.put(ENTITY1);
    batchWriter.put(entity);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test
  public void testPutAfterAdd() throws Exception {
    Entity entity = Entity.builder(ENTITY1).set("foo", "bar").build();
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addUpsert(entity.toPb())
        .build();
    batchWriter.add(ENTITY1);
    batchWriter.put(entity);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test
  public void testPutAfterUpdate() throws Exception {
    Entity entity = Entity.builder(ENTITY1).set("foo", "bar").build();
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addUpsert(entity.toPb())
        .build();
    batchWriter.update(ENTITY1);
    batchWriter.put(entity);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test
  public void testPutAfterDelete() throws Exception {
    Entity entity = Entity.builder(ENTITY1).set("foo", "bar").build();
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addUpsert(entity.toPb())
        .build();
    batchWriter.delete(KEY1);
    batchWriter.put(entity);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test(expected = DatastoreException.class)
  public void testPutWhenNotActive() throws Exception {
    batchWriter.deactivate();
    batchWriter.put(ENTITY1);
  }

  @Test
  public void testDelete() throws Exception {
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addDelete(KEY1.toPb())
        .addDelete(KEY2.toPb())
        .addDelete(KEY3.toPb())
        .build();
    batchWriter.delete(KEY1, KEY2);
    batchWriter.delete(KEY3);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test
  public void testDeleteAfterAdd() throws Exception {
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addInsertAutoId(INCOMPLETE_ENTITY_1.toPb())
        .addDelete(KEY1.toPb())
        .build();
    batchWriter.add(ENTITY1);
    batchWriter.addWithDeferredIdAllocation(INCOMPLETE_ENTITY_1);
    batchWriter.delete(KEY1);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test
  public void testDeleteAfterUpdate() throws Exception {
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addDelete(KEY1.toPb())
        .build();
    batchWriter.update(ENTITY1);
    batchWriter.delete(KEY1);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test
  public void testDeleteAfterPut() throws Exception {
    DatastoreV1.Mutation pb = DatastoreV1.Mutation.newBuilder()
        .addDelete(KEY1.toPb())
        .build();
    batchWriter.put(ENTITY1);
    batchWriter.delete(KEY1);
    assertEquals(pb, batchWriter.toMutationPb().build());
  }

  @Test(expected = DatastoreException.class)
  public void testDeleteWhenNotActive() throws Exception {
    batchWriter.deactivate();
    batchWriter.delete(KEY1);
  }
}

\ No newline at end of file
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BaseEntityTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BaseEntityTest.java
new file mode 100644
index 000000000000..daa0c502d4b5
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BaseEntityTest.java
@@ -0,0 +1 @@
+/*
 * Copyright 2015 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.gcloud.datastore;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

import org.junit.Before;
import org.junit.Test;

import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Set;

public class BaseEntityTest {

  private static final Blob BLOB = Blob.copyFrom(new byte[]{1, 2});
  private static final DateTime DATE_TIME = DateTime.now();
  private static final Key KEY = Key.builder("ds1", "k1", "n1").build();
  private static final Entity ENTITY = Entity.builder(KEY).set("name", "foo").build();
  private static final IncompleteKey INCOMPLETE_KEY = IncompleteKey.builder("ds1", "k1").build();
  private static final FullEntity PARTIAL_ENTITY =
      Entity.builder(INCOMPLETE_KEY).build();

  private Builder builder;

  private class Builder extends BaseEntity.Builder {

    @Override public BaseEntity build() {

      return new BaseEntity(this) {

        @Override
        protected Builder emptyBuilder() {
          return new BaseEntityTest.Builder();
        }
      };
    }
  }

  @Before
  public void setUp() {
    builder = new Builder();
    builder.set("blob", BLOB).set("boolean", true).set("dateTime", DATE_TIME);
    builder.set("double", 1.25).set("key", KEY).set("string", "hello world");
    builder.set("long", 125).setNull("null").set("entity", ENTITY);
    builder.set("partialEntity", PARTIAL_ENTITY).set("stringValue", StringValue.of("bla"));
    builder.set("list1", NullValue.of(), StringValue.of("foo"));
    builder.set("list2", ImmutableList.of(LongValue.of(10), DoubleValue.of(2)));
    builder.set("list3", Collections.singletonList(BooleanValue.of(true)));
  }

  @Test
  public void testContains() throws Exception {
    BaseEntity entity = builder.build();
    assertTrue(entity.contains("list1"));
    assertFalse(entity.contains("bla"));
    entity = builder.clear().build();
    assertFalse(entity.contains("list1"));
  }

  @Test
  public void testGetValue() throws Exception {
    BaseEntity entity = builder.build();
    assertEquals(BlobValue.of(BLOB), entity.getValue("blob"));
  }

  @Test(expected = DatastoreException.class)
  public void testGetValueNotFound() throws Exception {
    BaseEntity entity = builder.clear().build();
    entity.getValue("blob");
  }

  @Test
  public void testIsNull() throws Exception {
    BaseEntity entity = builder.build();
    assertTrue(entity.isNull("null"));
    assertFalse(entity.isNull("blob"));
    entity = builder.setNull("blob").build();
    assertTrue(entity.isNull("blob"));
  }

  @Test(expected = DatastoreException.class)
  public void testIsNullNotFound() throws Exception {
    BaseEntity entity = builder.clear().build();
    entity.isNull("null");
  }

  @Test
  public void testGetString() throws Exception {
    BaseEntity entity = builder.build();
    assertEquals("hello world", entity.getString("string"));
    assertEquals("bla", entity.getString("stringValue"));
    entity = builder.set("string", "foo").build();
    assertEquals("foo", entity.getString("string"));
  }

  @Test
  public void testGetLong() throws Exception {
    BaseEntity entity = builder.build();
    assertEquals(125, entity.getLong("long"));
    entity = builder.set("long", LongValue.of(10)).build();
    assertEquals(10, entity.getLong("long"));
  }

  @Test
  public void testGetDouble() throws Exception {
    BaseEntity entity = builder.build();
    assertEquals(1.25, entity.getDouble("double"), 0);
    entity = builder.set("double", DoubleValue.of(10)).build();
    assertEquals(10, entity.getDouble("double"), 0);
  }

  @Test
  public void testGetBoolean() throws Exception {
    BaseEntity entity = builder.build();
    assertTrue(entity.getBoolean("boolean"));
    entity = builder.set("boolean", BooleanValue.of(false)).build();
    assertFalse(entity.getBoolean("boolean"));
  }

  @Test
  public void testGetDateTime() throws Exception {
    BaseEntity entity = builder.build();
    assertEquals(DATE_TIME, entity.getDateTime("dateTime"));
    Calendar cal = Calendar.getInstance();
    cal.add(Calendar.DATE, -1);
    DateTime dateTime = DateTime.copyFrom(cal);
    entity = builder.set("dateTime", DateTimeValue.of(dateTime)).build();
    assertEquals(dateTime, entity.getDateTime("dateTime"));
  }

  @Test
  public void testGetKey() throws Exception {
    BaseEntity entity = builder.build();
    assertEquals(KEY, entity.getKey("key"));
    Key key = Key.builder(KEY).name("BLA").build();
    entity = builder.set("key", key).build();
    assertEquals(key, entity.getKey("key"));
  }

  @Test
  public void testGetEntity() throws Exception {
    BaseEntity entity = builder.build();
    assertEquals(ENTITY, entity.getEntity("entity"));
    assertEquals(PARTIAL_ENTITY, entity.getEntity("partialEntity"));
    entity = builder.set("entity", EntityValue.of(PARTIAL_ENTITY)).build();
    assertEquals(PARTIAL_ENTITY, entity.getEntity("entity"));
  }

  @Test
  public void testGetList() throws Exception {
    BaseEntity entity = builder.build();
    List> list = entity.getList("list1");
    assertEquals(2, list.size());
    assertEquals(NullValue.of(), list.get(0));
    assertEquals("foo", list.get(1).get());
    list = entity.getList("list2");
    assertEquals(2, list.size());
    assertEquals(Long.valueOf(10), list.get(0).get());
    assertEquals(Double.valueOf(2), list.get(1).get());
    list = entity.getList("list3");
    assertEquals(1, list.size());
    assertEquals(Boolean.TRUE, list.get(0).get());
    entity = builder.set("list1", ListValue.of(list)).build();
    assertEquals(list, entity.getList("list1"));
  }

  @Test
  public void testGetBlob() throws Exception {
    BaseEntity entity = builder.build();
    assertEquals(BLOB, entity.getBlob("blob"));
    Blob blob = Blob.copyFrom(new byte[] {});
    entity = builder.set("blob", BlobValue.of(blob)).build();
    assertEquals(blob, entity.getBlob("blob"));
  }

  @Test
  public void testNames() throws Exception {
    Set names = ImmutableSet.builder()
        .add("string", "stringValue", "boolean", "double", "long", "list1", "list2", "list3")
        .add("entity", "partialEntity", "null", "dateTime", "blob", "key")
        .build();
    BaseEntity entity = builder.build();
    assertEquals(names, entity.names());
  }
}
\ No newline at end of file
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BaseKeyTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BaseKeyTest.java
new file mode 100644
index 000000000000..e99e7a60fd0b
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BaseKeyTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BaseKeyTest {
+
+  private class Builder extends BaseKey.Builder {
+
+    Builder(String projectId) {
+      super(projectId);
+    }
+
+    Builder(String projectId, String kind) {
+      super(projectId, kind);
+    }
+
+    @Override
+    protected BaseKey build() {
+      ImmutableList.Builder path = ImmutableList.builder();
+      path.addAll(ancestors);
+      path.add(PathElement.of(kind));
+      return new BaseKey(projectId, namespace, path.build()) {
+        @Override
+        protected Object fromPb(byte[] bytesPb) throws InvalidProtocolBufferException {
+          return null;
+        }
+      };
+    }
+  }
+
+  @Test
+  public void testDataset() throws Exception {
+    Builder builder = new Builder("ds1", "k");
+    BaseKey key = builder.build();
+    assertEquals("ds1", key.projectId());
+    key = builder.projectId("ds2").build();
+    assertEquals("ds2", key.projectId());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBadDatasetInConstructor() throws Exception {
+    new Builder(" ", "k");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBadDatasetInSetter() throws Exception {
+    Builder builder = new Builder("d", "k");
+    builder.projectId(" ");
+  }
+
+  @Test
+  public void testNamespace() throws Exception {
+    Builder builder = new Builder("ds", "k");
+    BaseKey key = builder.build();
+    assertNull(key.namespace());
+    key = builder.namespace("ns").build();
+    assertEquals("ns", key.namespace());
+  }
+
+  @Test
+  public void testKind() throws Exception {
+    Builder builder = new Builder("ds", "k1");
+    BaseKey key = builder.build();
+    assertEquals("k1", key.kind());
+    key = builder.kind("k2").build();
+    assertEquals("k2", key.kind());
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testNoKind() throws Exception {
+    Builder builder = new Builder("ds");
+    builder.build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBadKindInConstructor() throws Exception {
+    new Builder("ds", "");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBadKindInSetter() throws Exception {
+    Builder builder = new Builder("ds", "k1");
+    builder.kind("");
+  }
+
+  @Test
+  public void testAncestors() throws Exception {
+    Builder builder = new Builder("ds", "k");
+    BaseKey key = builder.build();
+    assertTrue(key.ancestors().isEmpty());
+    List path = new ArrayList<>();
+    path.add(PathElement.of("p1","v1"));
+    key = builder.ancestors(path.get(0)).build();
+    assertEquals(path, key.ancestors());
+    path.add(PathElement.of("p2","v2"));
+    key = builder.ancestors(path.get(1)).build();
+    assertEquals(path, key.ancestors());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BlobTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BlobTest.java
new file mode 100644
index 000000000000..e78023101e48
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BlobTest.java
@@ -0,0 +1 @@
+/*
 * Copyright 2015 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.gcloud.datastore;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;

import org.junit.Before;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Random;

public class BlobTest {

  private byte[] bytes1 = new byte[10];
  private byte[] bytes2 = new byte[11];
  private Blob blob1;
  private Blob blob2;


  @Before
  public void setUp() {
    Random rnd = new Random();
    rnd.nextBytes(bytes1);
    rnd.nextBytes(bytes2);
    blob1 = Blob.copyFrom(bytes1);
    blob2 = Blob.copyFrom(bytes2);
  }

  @Test
  public void testEquals() throws Exception {
    assertEquals(blob1, blob1);
    assertEquals(blob1, Blob.copyFrom(bytes1));
    assertNotEquals(blob1, blob2);
  }

  @Test
  public void testLength() throws Exception {
    assertEquals(bytes1.length, blob1.length());
    assertEquals(bytes2.length, blob2.length());
  }

  @Test
  public void testToByteArray() throws Exception {
    assertArrayEquals(bytes1, blob1.toByteArray());
    assertArrayEquals(bytes2, blob2.toByteArray());
  }

  @Test
  public void testAsReadOnlyByteBuffer() throws Exception {
    ByteBuffer buffer = blob1.asReadOnlyByteBuffer();
    byte[] bytes = new byte[bytes1.length];
    buffer.get(bytes);
    assertFalse(buffer.hasRemaining());
    assertArrayEquals(bytes1, bytes);
  }

  @Test
  public void testAsInputStream() throws Exception {
    byte[] bytes = new byte[bytes1.length];
    InputStream in = blob1.asInputStream();
    assertEquals(bytes1.length, in.read(bytes));
    assertEquals(-1, in.read());
    assertArrayEquals(bytes1, bytes);
  }

  @Test
  public void testCopyTo() throws Exception {
    byte[] bytes = new byte[bytes1.length];
    blob1.copyTo(bytes);
    assertArrayEquals(bytes1, bytes);

    ByteBuffer buffer = ByteBuffer.allocate(bytes1.length);
    blob1.copyTo(buffer);
    buffer.flip();
    bytes = new byte[bytes1.length];
    buffer.get(bytes);
    assertFalse(buffer.hasRemaining());
    assertArrayEquals(bytes1, bytes);
  }

  @Test
  public void testCopyFrom() throws Exception {
    Blob blob = Blob.copyFrom(ByteBuffer.wrap(bytes1));
    assertEquals(blob1, blob);
    assertArrayEquals(bytes1, blob.toByteArray());

    blob = Blob.copyFrom(new ByteArrayInputStream(bytes2));
    assertEquals(blob2, blob);
    assertArrayEquals(bytes2, blob.toByteArray());
  }
}
\ No newline at end of file
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BlobValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BlobValueTest.java
new file mode 100644
index 000000000000..40d0299d8fb3
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BlobValueTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class BlobValueTest {
+
+  private static final Blob CONTENT = Blob.copyFrom(new byte[] {1, 2});
+
+  @Test
+  public void testToBuilder() throws Exception {
+    BlobValue value = BlobValue.of(CONTENT);
+    assertEquals(value, value.toBuilder().build());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testOf() throws Exception {
+    BlobValue value = BlobValue.of(CONTENT);
+    assertEquals(CONTENT, value.get());
+    assertFalse(value.hasIndexed());
+    assertFalse(value.hasMeaning());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testBuilder() throws Exception {
+    BlobValue.Builder builder = BlobValue.builder(CONTENT);
+    BlobValue value = builder.meaning(1).indexed(false).build();
+    assertEquals(CONTENT, value.get());
+    assertTrue(value.hasMeaning());
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertTrue(value.hasIndexed());
+    assertFalse(value.indexed());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BooleanValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BooleanValueTest.java
new file mode 100644
index 000000000000..16bbe9cbf518
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/BooleanValueTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class BooleanValueTest {
+
+  @Test
+  public void testToBuilder() throws Exception {
+    BooleanValue value = BooleanValue.of(true);
+    assertEquals(value, value.toBuilder().build());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testOf() throws Exception {
+    BooleanValue value = BooleanValue.of(false);
+    assertFalse(value.get());
+    assertFalse(value.hasIndexed());
+    assertFalse(value.hasMeaning());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testBuilder() throws Exception {
+    BooleanValue.Builder builder = BooleanValue.builder(true);
+    BooleanValue value = builder.meaning(1).indexed(true).build();
+    assertTrue(value.get());
+    assertTrue(value.hasMeaning());
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertTrue(value.hasIndexed());
+    assertTrue(value.indexed());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/CursorTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/CursorTest.java
new file mode 100644
index 000000000000..6806fd698331
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/CursorTest.java
@@ -0,0 +1 @@
+/*
 * Copyright 2015 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.gcloud.datastore;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;

import com.google.protobuf.ByteString;

import org.junit.Before;
import org.junit.Test;

public class CursorTest {

  private byte[] bytes1 = {1, 2, 3, '%', '<', '+'};
  private byte[] bytes2 = {10, 20, 30};
  private Cursor cursor1;
  private Cursor cursor2;

  @Before
  public void setUp() throws Exception {
    cursor1 = new Cursor(ByteString.copyFrom(bytes1));
    cursor2 = new Cursor(ByteString.copyFrom(bytes2));
  }

  @Test
  public void testToFromUrlSafe() throws Exception {
    String urlSafe = cursor1.toUrlSafe();
    assertEquals(cursor1, Cursor.fromUrlSafe(urlSafe));
  }

  @Test
  public void testCopyFrom() throws Exception {
    Cursor cursor = Cursor.copyFrom(bytes2);
    assertEquals(cursor2, cursor);
    assertNotEquals(cursor1, cursor);
  }
}
\ No newline at end of file
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreExceptionTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreExceptionTest.java
new file mode 100644
index 000000000000..1dd0f255ceca
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreExceptionTest.java
@@ -0,0 +1 @@
+/*
 * Copyright 2015 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.gcloud.datastore;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import com.google.gcloud.datastore.DatastoreException.Code;
import com.google.gcloud.spi.DatastoreRpc.DatastoreRpcException;
import com.google.gcloud.spi.DatastoreRpc.DatastoreRpcException.Reason;

import org.junit.Test;

public class DatastoreExceptionTest {

  @Test
  public void testCode() throws Exception {
    for (Reason reason : Reason.values()) {
      Code code = Code.valueOf(reason.name());
      assertEquals(reason.retryable(), code.retryable());
      assertEquals(reason.description(), code.description());
      assertEquals(reason.httpStatus(), code.httpStatus());
    }

    DatastoreException exception = new DatastoreException(Code.ABORTED, "bla");
    assertEquals(Code.ABORTED, exception.code());
  }

  @Test
  public void testTranslateAndThrow() throws Exception {
    for (Reason reason : Reason.values()) {
      try {
        DatastoreException.translateAndThrow(new DatastoreRpcException(reason));
        fail("Exception expected");
      } catch (DatastoreException ex) {
        assertEquals(reason.name(), ex.code().name());
      }
    }
  }

  @Test
  public void testThrowInvalidRequest() throws Exception {
    try {
      DatastoreException.throwInvalidRequest("message %s %d", "a", 1);
      fail("Exception expected");
    } catch (DatastoreException ex) {
      assertEquals(Code.FAILED_PRECONDITION, ex.code());
      assertEquals("message a 1", ex.getMessage());
    }
  }
}
\ No newline at end of file
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreHelperTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreHelperTest.java
new file mode 100644
index 000000000000..55c8d0cf3ce6
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreHelperTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Iterators;
+import com.google.gcloud.datastore.Datastore.TransactionCallable;
+
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+public class DatastoreHelperTest {
+
+  @Test
+  public void testNewKeyFactory() {
+    DatastoreOptions options = createMock(DatastoreOptions.class);
+    expect(options.projectId()).andReturn("ds1").once();
+    expect(options.namespace()).andReturn("ns1").once();
+    replay(options);
+    KeyFactory keyFactory = DatastoreHelper.newKeyFactory(options);
+    Key key = keyFactory.kind("k").newKey("bla");
+    assertEquals("ds1", key.projectId());
+    assertEquals("ns1", key.namespace());
+    assertEquals("k", key.kind());
+    assertEquals("bla", key.name());
+    verify(options);
+  }
+
+  @Test
+  public void testAllocateId() throws Exception {
+    Datastore datastore = createStrictMock(Datastore.class);
+    IncompleteKey pKey1 = IncompleteKey.builder("ds", "k").build();
+    Key key1 = Key.builder(pKey1, 1).build();
+    expect(datastore.allocateId(new IncompleteKey[] {pKey1}))
+        .andReturn(Collections.singletonList(key1));
+    replay(datastore);
+    assertEquals(key1, DatastoreHelper.allocateId(datastore, pKey1));
+    verify(datastore);
+  }
+
+  @Test
+  public void testGet() throws Exception {
+    Datastore datastore = createStrictMock(Datastore.class);
+    IncompleteKey pKey1 = IncompleteKey.builder("ds", "k").build();
+    Key key1 = Key.builder(pKey1, 1).build();
+    Entity entity1 = Entity.builder(key1).build();
+    Key key2 = Key.builder(pKey1, 2).build();
+    expect(datastore.get(new Key[]{key1}))
+        .andReturn(Collections.singletonList(entity1).iterator());
+    expect(datastore.get(new Key[]{key2}))
+        .andReturn(Collections.emptyIterator());
+    replay(datastore);
+    assertEquals(entity1, DatastoreHelper.get(datastore, key1));
+    assertNull(DatastoreHelper.get(datastore, key2));
+    verify(datastore);
+  }
+
+  @Test
+  public void testAdd() throws Exception {
+    Datastore datastore = createStrictMock(Datastore.class);
+    IncompleteKey pKey = IncompleteKey.builder("ds", "k").build();
+    Key key = Key.builder(pKey, 1).build();
+    Entity entity = Entity.builder(key).build();
+    expect(datastore.add(new Entity[]{entity}))
+        .andReturn(Collections.singletonList(entity));
+    replay(datastore);
+    assertEquals(entity, DatastoreHelper.add(datastore, entity));
+    verify(datastore);
+  }
+
+  @Test
+  public void testFetch() throws Exception {
+    Datastore datastore = createStrictMock(Datastore.class);
+    IncompleteKey pKey1 = IncompleteKey.builder("ds", "k").build();
+    Key key1 = Key.builder(pKey1, 1).build();
+    Key key2 = Key.builder(pKey1, "a").build();
+    Entity entity1 = Entity.builder(key1).build();
+    Entity entity2 = Entity.builder(key2).build();
+    expect(datastore.get(key1, key2)).andReturn(Iterators.forArray(entity1, entity2)).once();
+    replay(datastore);
+    List values = DatastoreHelper.fetch(datastore, key1, key2);
+    assertEquals(2, values.size());
+    assertEquals(entity1, values.get(0));
+    assertEquals(entity2, values.get(1));
+    verify(datastore);
+  }
+
+  @Test
+  public void testRunInTransaction() throws Exception {
+    final Datastore datastore = createStrictMock(Datastore.class);
+    final Transaction transaction = createStrictMock(Transaction.class);
+    expect(datastore.newTransaction()).andReturn(transaction).once();
+    expect(transaction.active()).andReturn(true).once();
+    expect(transaction.commit()).andReturn(null).once();
+    expect(transaction.active()).andReturn(false).once();
+    replay(datastore, transaction);
+    String value = DatastoreHelper.runInTransaction(datastore,
+        new TransactionCallable() {
+          @Override
+          public String run(DatastoreReaderWriter readerWriter) {
+            assertTrue(transaction.active());
+            assertSame(transaction, readerWriter);
+            return "done";
+          }
+        });
+    verify(datastore, transaction);
+    assertEquals("done", value);
+  }
+
+  @Test
+  public void testRunInTransactionWithException() throws Exception {
+    final Datastore datastore = createStrictMock(Datastore.class);
+    final Transaction transaction = createStrictMock(Transaction.class);
+    expect(datastore.newTransaction()).andReturn(transaction).once();
+    expect(transaction.active()).andReturn(true).once();
+    transaction.rollback();
+    EasyMock.expectLastCall().once();
+    expect(transaction.active()).andReturn(false).once();
+    replay(datastore, transaction);
+    try {
+      DatastoreHelper.runInTransaction(datastore, new TransactionCallable() {
+        @Override
+        public Void run(DatastoreReaderWriter readerWriter) throws Exception {
+          assertTrue(transaction.active());
+          assertSame(transaction, readerWriter);
+          throw new Exception("Bla");
+        }
+      });
+      fail("DatastoreException was expected");
+    } catch (DatastoreException ex) {
+      assertEquals("Bla", ex.getCause().getMessage());
+    }
+    verify(datastore, transaction);
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreOptionsTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreOptionsTest.java
new file mode 100644
index 000000000000..e7dc71c50ff6
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreOptionsTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gcloud.spi.DatastoreRpc;
+import com.google.gcloud.spi.DatastoreRpcFactory;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class DatastoreOptionsTest {
+
+  private static final String PROJECT_ID = "project_id";
+  private DatastoreRpcFactory datastoreRpcFactory;
+  private DatastoreRpc datastoreRpc;
+  private DatastoreOptions.Builder options;
+
+  @Before
+  public void setUp() throws IOException, InterruptedException {
+    datastoreRpcFactory = EasyMock.createMock(DatastoreRpcFactory.class);
+    datastoreRpc = EasyMock.createMock(DatastoreRpc.class);
+    options = DatastoreOptions.builder()
+        .normalizeDataset(false)
+        .serviceRpcFactory(datastoreRpcFactory)
+        .projectId(PROJECT_ID)
+        .host("http://localhost:" + LocalGcdHelper.PORT);
+    EasyMock.expect(datastoreRpcFactory.create(EasyMock.anyObject(DatastoreOptions.class)))
+        .andReturn(datastoreRpc)
+        .anyTimes();
+    EasyMock.replay(datastoreRpcFactory, datastoreRpc);
+  }
+
+  @Test
+  public void testProjectId() throws Exception {
+    assertEquals(PROJECT_ID, options.build().projectId());
+  }
+
+  @Test
+  public void testHost() throws Exception {
+    assertEquals("http://localhost:" + LocalGcdHelper.PORT, options.build().host());
+  }
+
+  @Test
+  public void testNamespace() throws Exception {
+    assertNull(options.build().namespace());
+    assertEquals("ns1", options.namespace("ns1").build().namespace());
+  }
+
+  @Test
+  public void testForce() throws Exception {
+    assertFalse(options.build().force());
+    assertTrue(options.force(true).build().force());
+  }
+
+  @Test
+  public void testDatastore() throws Exception {
+    assertSame(datastoreRpcFactory, options.build().serviceRpcFactory());
+    assertSame(datastoreRpc, options.build().datastoreRpc());
+  }
+
+  @Test
+  public void testToBuilder() throws Exception {
+    DatastoreOptions original = options.namespace("ns1").force(true).build();
+    DatastoreOptions copy = original.toBuilder().build();
+    assertEquals(original.projectId(), copy.projectId());
+    assertEquals(original.namespace(), copy.namespace());
+    assertEquals(original.host(), copy.host());
+    assertEquals(original.force(), copy.force());
+    assertEquals(original.retryParams(), copy.retryParams());
+    assertEquals(original.authCredentials(), copy.authCredentials());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreTest.java
new file mode 100644
index 000000000000..156f9684f8ba
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DatastoreTest.java
@@ -0,0 +1,660 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.api.services.datastore.DatastoreV1;
+import com.google.api.services.datastore.DatastoreV1.EntityResult;
+import com.google.common.collect.Iterators;
+import com.google.gcloud.RetryParams;
+import com.google.gcloud.datastore.Query.ResultType;
+import com.google.gcloud.datastore.StructuredQuery.OrderBy;
+import com.google.gcloud.datastore.StructuredQuery.Projection;
+import com.google.gcloud.datastore.StructuredQuery.PropertyFilter;
+import com.google.gcloud.spi.DatastoreRpc;
+import com.google.gcloud.spi.DatastoreRpc.DatastoreRpcException.Reason;
+import com.google.gcloud.spi.DatastoreRpcFactory;
+
+import org.easymock.EasyMock;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class DatastoreTest {
+
+  private static final String PROJECT_ID = LocalGcdHelper.DEFAULT_PROJECT_ID;
+  private static final String KIND1 = "kind1";
+  private static final String KIND2 = "kind2";
+  private static final String KIND3 = "kind3";
+  private static final NullValue NULL_VALUE = NullValue.of();
+  private static final StringValue STR_VALUE = StringValue.of("str");
+  private static final BooleanValue BOOL_VALUE = BooleanValue.builder(false).indexed(false).build();
+  private static final IncompleteKey INCOMPLETE_KEY1 =
+      IncompleteKey.builder(PROJECT_ID, KIND1).build();
+  private static final IncompleteKey INCOMPLETE_KEY2 =
+      IncompleteKey.builder(PROJECT_ID, KIND2).build();
+  private static final Key KEY1 = Key.builder(INCOMPLETE_KEY1, "name").build();
+  private static final Key KEY2 = Key.builder(KEY1, KIND2, 1).build();
+  private static final Key KEY3 = Key.builder(KEY2).name("bla").build();
+  private static final Key KEY4 = Key.builder(KEY2).name("newName1").build();
+  private static final Key KEY5 = Key.builder(KEY2).name("newName2").build();
+  private static final KeyValue KEY_VALUE = KeyValue.of(KEY1);
+  private static final ListValue LIST_VALUE1 = ListValue.builder()
+      .addValue(NULL_VALUE)
+      .addValue(STR_VALUE, BOOL_VALUE)
+      .build();
+  private static final ListValue LIST_VALUE2 = ListValue.of(Collections.singletonList(KEY_VALUE));
+  private static final DateTimeValue DATE_TIME_VALUE = new DateTimeValue(DateTime.now());
+  private static final FullEntity PARTIAL_ENTITY1 =
+      FullEntity.builder(INCOMPLETE_KEY2).set("str", STR_VALUE).set("bool", BOOL_VALUE)
+          .set("list", LIST_VALUE1).build();
+  private static final FullEntity PARTIAL_ENTITY2 =
+      FullEntity.builder(PARTIAL_ENTITY1).remove("str").set("bool", true).
+          set("list", LIST_VALUE1.get()).build();
+  private static final FullEntity PARTIAL_ENTITY3 =
+      FullEntity.builder(PARTIAL_ENTITY1).key(IncompleteKey.builder(PROJECT_ID, KIND3).build())
+          .build();
+  private static final Entity ENTITY1 = Entity.builder(KEY1)
+      .set("str", STR_VALUE)
+      .set("date", DATE_TIME_VALUE)
+      .set("bool", BOOL_VALUE)
+      .set("partial1", EntityValue.of(PARTIAL_ENTITY1))
+      .set("list", LIST_VALUE2)
+      .build();
+  private static final Entity ENTITY2 = Entity.builder(ENTITY1).key(KEY2).remove("str")
+      .set("name", "Dan").setNull("null").set("age", 20).build();
+  private static final Entity ENTITY3 = Entity.builder(ENTITY1).key(KEY3).remove("str")
+      .set("null", NULL_VALUE).set("partial1", PARTIAL_ENTITY2).set("partial2", ENTITY2).build();
+
+  private DatastoreOptions options;
+  private Datastore datastore;
+
+  private static LocalGcdHelper gcdHelper;
+
+  @BeforeClass
+  public static void beforeClass() throws IOException, InterruptedException {
+    if (!LocalGcdHelper.isActive(PROJECT_ID)) {
+      gcdHelper = LocalGcdHelper.start(PROJECT_ID);
+    }
+  }
+
+  @Before
+  public void setUp() throws IOException, InterruptedException {
+    options = DatastoreOptions.builder()
+        .projectId(PROJECT_ID)
+        .host("http://localhost:" + LocalGcdHelper.PORT)
+        .build();
+    datastore = DatastoreFactory.instance().get(options);
+    StructuredQuery query = Query.keyQueryBuilder().build();
+    QueryResults result = datastore.run(query);
+    datastore.delete(Iterators.toArray(result, Key.class));
+    datastore.add(ENTITY1, ENTITY2);
+  }
+
+  @AfterClass
+  public static void afterClass() throws IOException, InterruptedException {
+    if (gcdHelper != null) {
+      gcdHelper.stop();
+    }
+  }
+
+  @Test
+  public void testGetOptions() {
+    assertSame(options, datastore.options());
+  }
+
+  @Test
+  public void testNewTransactionCommit() {
+    Transaction transaction = datastore.newTransaction();
+    transaction.add(ENTITY3);
+    Entity entity2 = Entity.builder(ENTITY2)
+        .clear()
+        .setNull("bla")
+        .build();
+    transaction.update(entity2);
+    transaction.delete(KEY1);
+    transaction.commit();
+
+    List list = datastore.fetch(KEY1, KEY2, KEY3);
+    assertNull(list.get(0));
+    assertEquals(entity2, list.get(1));
+    assertEquals(ENTITY3, list.get(2));
+    assertEquals(3, list.size());
+
+    try {
+      transaction.commit();
+      fail("Expecting a failure");
+    } catch (DatastoreException ex) {
+      // expected to fail
+    }
+
+    try {
+      transaction.rollback();
+      fail("Expecting a failure");
+    } catch (DatastoreException ex) {
+      // expected to fail
+    }
+
+    verifyNotUsable(transaction);
+  }
+
+  @Test
+  public void testTransactionWithRead() {
+    Transaction transaction = datastore.newTransaction();
+    assertNull(transaction.get(KEY3));
+    transaction.add(ENTITY3);
+    transaction.commit();
+    assertEquals(ENTITY3, datastore.get(KEY3));
+
+    transaction = datastore.newTransaction();
+    assertEquals(ENTITY3, transaction.get(KEY3));
+    // update entity3 during the transaction
+    datastore.put(Entity.builder(ENTITY3).clear().build());
+    transaction.update(ENTITY2);
+    try {
+      transaction.commit();
+      fail("Expecting a failure");
+    } catch (DatastoreException expected) {
+      assertEquals(DatastoreException.Code.ABORTED, expected.code());
+    }
+  }
+
+  @Test
+  public void testTransactionWithQuery() {
+    Query query = Query.entityQueryBuilder()
+        .kind(KIND2)
+        .filter(PropertyFilter.hasAncestor(KEY2))
+        .build();
+    Transaction transaction = datastore.newTransaction();
+    QueryResults results = transaction.run(query);
+    assertEquals(ENTITY2, results.next());
+    assertFalse(results.hasNext());
+    transaction.add(ENTITY3);
+    transaction.commit();
+    assertEquals(ENTITY3, datastore.get(KEY3));
+
+    transaction = datastore.newTransaction();
+    results = transaction.run(query);
+    assertEquals(ENTITY2, results.next());
+    transaction.delete(ENTITY3.key());
+    // update entity2 during the transaction
+    datastore.put(Entity.builder(ENTITY2).clear().build());
+    try {
+      transaction.commit();
+      fail("Expecting a failure");
+    } catch (DatastoreException expected) {
+      assertEquals(DatastoreException.Code.ABORTED, expected.code());
+    }
+  }
+
+  @Test
+  public void testNewTransactionRollback() {
+    Transaction transaction = datastore.newTransaction();
+    transaction.add(ENTITY3);
+    Entity entity2 = Entity.builder(ENTITY2).clear().setNull("bla")
+        .set("list3", StringValue.of("bla"), StringValue.builder("bla").build()).build();
+    transaction.update(entity2);
+    transaction.delete(KEY1);
+    transaction.rollback();
+    transaction.rollback(); // should be safe to repeat rollback calls
+
+    try {
+      transaction.commit();
+      fail("Expecting a failure");
+    } catch (DatastoreException ex) {
+      // expected to fail
+    }
+
+    verifyNotUsable(transaction);
+
+    List list = datastore.fetch(KEY1, KEY2, KEY3);
+    assertEquals(ENTITY1, list.get(0));
+    assertEquals(ENTITY2, list.get(1));
+    assertNull(list.get(2));
+    assertEquals(3, list.size());
+  }
+
+  private void verifyNotUsable(DatastoreWriter writer) {
+    try {
+      writer.add(ENTITY3);
+      fail("Expecting a failure");
+    } catch (DatastoreException ex) {
+      // expected to fail
+    }
+
+    try {
+      writer.put(ENTITY3);
+      fail("Expecting a failure");
+    } catch (DatastoreException ex) {
+      // expected to fail
+    }
+
+    try {
+      writer.update(ENTITY3);
+      fail("Expecting a failure");
+    } catch (DatastoreException ex) {
+      // expected to fail
+    }
+
+    try {
+      writer.delete(ENTITY3.key());
+      fail("Expecting a failure");
+    } catch (DatastoreException ex) {
+      // expected to fail
+    }
+  }
+
+  @Test
+  public void testNewBatch() {
+    Batch batch = datastore.newBatch();
+    Entity entity1 = Entity.builder(ENTITY1).clear().build();
+    Entity entity2 = Entity.builder(ENTITY2).clear().setNull("bla").build();
+    Entity entity4 = Entity.builder(KEY4).set("value", StringValue.of("value")).build();
+    Entity entity5 = Entity.builder(KEY5).set("value", "value").build();
+
+    List entities = batch.add(entity4, PARTIAL_ENTITY2, entity5);
+    Entity entity6 = entities.get(1);
+    assertSame(entity4, entities.get(0));
+    assertEquals(PARTIAL_ENTITY2.properties(), entity6.properties());
+    assertEquals(PARTIAL_ENTITY2.key().projectId(), entity6.key().projectId());
+    assertEquals(PARTIAL_ENTITY2.key().namespace(), entity6.key().namespace());
+    assertEquals(PARTIAL_ENTITY2.key().ancestors(), entity6.key().ancestors());
+    assertEquals(PARTIAL_ENTITY2.key().kind(), entity6.key().kind());
+    assertEquals(PARTIAL_ENTITY2.key(), IncompleteKey.builder(entity6.key()).build());
+    assertNotEquals(PARTIAL_ENTITY2.key().path(), entity6.key().path());
+    assertNotEquals(PARTIAL_ENTITY2.key(), entity6.key());
+    assertSame(entity5, entities.get(2));
+    batch.addWithDeferredIdAllocation(PARTIAL_ENTITY3);
+    batch.put(ENTITY3, entity1, entity2);
+
+    Batch.Response response = batch.submit();
+    entities = datastore.fetch(KEY1, KEY2, KEY3, entity4.key(), entity5.key(), entity6.key());
+    assertEquals(entity1, entities.get(0));
+    assertEquals(entity2, entities.get(1));
+    assertEquals(ENTITY3, entities.get(2));
+    assertEquals(entity4, entities.get(3));
+    assertEquals(entity5, entities.get(4));
+    assertEquals(entity6, entities.get(5));
+    assertEquals(6, entities.size());
+    List generatedKeys = response.generatedKeys();
+    assertEquals(1, generatedKeys.size());
+    assertEquals(PARTIAL_ENTITY3.properties(), datastore.get(generatedKeys.get(0)).properties());
+    assertEquals(PARTIAL_ENTITY3.key(), IncompleteKey.builder(generatedKeys.get(0)).build());
+
+    try {
+      batch.submit();
+      fail("Expecting a failure");
+    } catch (DatastoreException ex) {
+      // expected to fail
+    }
+    verifyNotUsable(batch);
+
+    batch = datastore.newBatch();
+    batch.delete(entity4.key(), entity5.key());
+    batch.update(ENTITY1, ENTITY2, ENTITY3);
+    batch.submit();
+    entities = datastore.fetch(KEY1, KEY2, KEY3, entity4.key(), entity5.key());
+    assertEquals(ENTITY1, entities.get(0));
+    assertEquals(ENTITY2, entities.get(1));
+    assertEquals(ENTITY3, entities.get(2));
+    assertNull(entities.get(3));
+    assertNull(entities.get(4));
+    assertEquals(5, entities.size());
+  }
+
+  @Test
+  public void testRunGqlQueryNoCasting() {
+    Query query1 = Query.gqlQueryBuilder(ResultType.ENTITY, "select * from " + KIND1).build();
+    QueryResults results1 = datastore.run(query1);
+    assertTrue(results1.hasNext());
+    assertEquals(ENTITY1, results1.next());
+    assertFalse(results1.hasNext());
+
+    datastore.put(ENTITY3);
+    Query query2 =  Query.gqlQueryBuilder(
+        ResultType.ENTITY, "select * from " + KIND2 + " order by __key__").build();
+    QueryResults results2 = datastore.run(query2);
+    assertTrue(results2.hasNext());
+    assertEquals(ENTITY2, results2.next());
+    assertTrue(results2.hasNext());
+    assertEquals(ENTITY3, results2.next());
+    assertFalse(results2.hasNext());
+
+    query1 = Query.gqlQueryBuilder(ResultType.ENTITY, "select * from bla").build();
+    results1 = datastore.run(query1);
+    assertFalse(results1.hasNext());
+
+    Query keyOnlyQuery =
+        Query.gqlQueryBuilder(ResultType.KEY, "select __key__ from " + KIND1).build();
+    QueryResults keyOnlyResults = datastore.run(keyOnlyQuery);
+    assertTrue(keyOnlyResults.hasNext());
+    assertEquals(KEY1, keyOnlyResults.next());
+    assertFalse(keyOnlyResults.hasNext());
+
+    GqlQuery keyProjectionQuery = Query.gqlQueryBuilder(
+        ResultType.PROJECTION_ENTITY, "select __key__ from " + KIND1).build();
+    QueryResults keyProjectionResult = datastore.run(keyProjectionQuery);
+    assertTrue(keyProjectionResult.hasNext());
+    ProjectionEntity projectionEntity = keyProjectionResult.next();
+    assertEquals(KEY1, projectionEntity.key());
+    assertTrue(projectionEntity.properties().isEmpty());
+    assertFalse(keyProjectionResult.hasNext());
+
+    GqlQuery projectionQuery = Query.gqlQueryBuilder(
+        ResultType.PROJECTION_ENTITY, "select str, date from " + KIND1).build();
+
+    QueryResults projectionResult = datastore.run(projectionQuery);
+    assertTrue(projectionResult.hasNext());
+    projectionEntity = projectionResult.next();
+    assertEquals("str", projectionEntity.getString("str"));
+    assertEquals(DATE_TIME_VALUE.get(), projectionEntity.getDateTime("date"));
+    assertEquals(DATE_TIME_VALUE.get().timestampMicroseconds(),
+        projectionEntity.getLong("date"));
+    assertEquals(2, projectionEntity.names().size());
+    assertFalse(projectionResult.hasNext());
+  }
+
+  @Test
+  public void testRunGqlQueryWithCasting() {
+    @SuppressWarnings("unchecked")
+    Query query1 =
+        (Query) Query.gqlQueryBuilder("select * from " + KIND1).build();
+    QueryResults results1 = datastore.run(query1);
+    assertTrue(results1.hasNext());
+    assertEquals(ENTITY1, results1.next());
+    assertFalse(results1.hasNext());
+
+    Query query2 = Query.gqlQueryBuilder("select * from " + KIND1).build();
+    QueryResults results2 = datastore.run(query2);
+    assertSame(Entity.class, results2.resultClass());
+    @SuppressWarnings("unchecked")
+    QueryResults results3 = (QueryResults) results2;
+    assertTrue(results3.hasNext());
+    assertEquals(ENTITY1, results3.next());
+    assertFalse(results3.hasNext());
+  }
+
+  @Test
+  public void testRunStructuredQuery() {
+    Query query =
+        Query.entityQueryBuilder().kind(KIND1).orderBy(OrderBy.asc("__key__")).build();
+    QueryResults results1 = datastore.run(query);
+    assertTrue(results1.hasNext());
+    assertEquals(ENTITY1, results1.next());
+    assertFalse(results1.hasNext());
+
+    Query keyOnlyQuery =  Query.keyQueryBuilder().kind(KIND1).build();
+    QueryResults results2 = datastore.run(keyOnlyQuery);
+    assertTrue(results2.hasNext());
+    assertEquals(ENTITY1.key(), results2.next());
+    assertFalse(results2.hasNext());
+
+    StructuredQuery keyOnlyProjectionQuery =
+        Query.projectionEntityQueryBuilder()
+        .kind(KIND1).projection(Projection.property("__key__")).build();
+    QueryResults results3 = datastore.run(keyOnlyProjectionQuery);
+    assertTrue(results3.hasNext());
+    ProjectionEntity projectionEntity = results3.next();
+    assertEquals(ENTITY1.key(), projectionEntity.key());
+    assertTrue(projectionEntity.names().isEmpty());
+    assertFalse(results2.hasNext());
+
+    StructuredQuery projectionQuery = Query.projectionEntityQueryBuilder()
+        .kind(KIND2)
+        .projection(Projection.property("age"), Projection.first("name"))
+        .filter(PropertyFilter.gt("age", 18))
+        .groupBy("age")
+        .orderBy(OrderBy.asc("age"))
+        .limit(10)
+        .build();
+
+    QueryResults results4 = datastore.run(projectionQuery);
+    assertTrue(results4.hasNext());
+    ProjectionEntity entity = results4.next();
+    assertEquals(ENTITY2.key(), entity.key());
+    assertEquals(20, entity.getLong("age"));
+    assertEquals("Dan", entity.getString("name"));
+    assertEquals(2, entity.properties().size());
+    assertFalse(results4.hasNext());
+    // TODO(ozarov): construct a test to verify nextQuery/pagination
+  }
+
+  @Test
+  public void testAllocateId() {
+    KeyFactory keyFactory = datastore.newKeyFactory().kind(KIND1);
+    IncompleteKey pk1 = keyFactory.newKey();
+    Key key1 = datastore.allocateId(pk1);
+    assertEquals(key1.projectId(), pk1.projectId());
+    assertEquals(key1.namespace(), pk1.namespace());
+    assertEquals(key1.ancestors(), pk1.ancestors());
+    assertEquals(key1.kind(), pk1.kind());
+    assertTrue(key1.hasId());
+    assertFalse(key1.hasName());
+    assertEquals(Key.builder(pk1, key1.id()).build(), key1);
+
+    Key key2 = datastore.allocateId(pk1);
+    assertNotEquals(key1, key2);
+    assertEquals(Key.builder(pk1, key2.id()).build(), key2);
+
+    Key key3 = datastore.allocateId(key1);
+    assertNotEquals(key1, key3);
+    assertEquals(Key.builder(pk1, key3.id()).build(), key3);
+  }
+
+  @Test
+  public void testAllocateIdArray() {
+    KeyFactory keyFactory = datastore.newKeyFactory().kind(KIND1);
+    IncompleteKey incompleteKey1 = keyFactory.newKey();
+    IncompleteKey incompleteKey2 =
+        keyFactory.kind(KIND2).ancestors(PathElement.of(KIND1, 10)).newKey();
+    Key key3 = keyFactory.newKey("name");
+    Key key4 = keyFactory.newKey(1);
+    List result =
+        datastore.allocateId(incompleteKey1, incompleteKey2, key3, key4, incompleteKey1, key3);
+    assertEquals(6, result.size());
+    assertEquals(Key.builder(incompleteKey1, result.get(0).id()).build(), result.get(0));
+    assertEquals(Key.builder(incompleteKey1, result.get(4).id()).build(), result.get(4));
+    assertEquals(Key.builder(incompleteKey2, result.get(1).id()).build(), result.get(1));
+    assertEquals(Key.builder(key3).id(result.get(2).id()).build(), result.get(2));
+    assertEquals(Key.builder(key3).id(result.get(5).id()).build(), result.get(5));
+    assertEquals(Key.builder(key4).id(result.get(3).id()).build(), result.get(3));
+  }
+
+  @Test
+  public void testGet() {
+    Entity entity = datastore.get(KEY3);
+    assertNull(entity);
+
+    entity = datastore.get(KEY1);
+    assertEquals(ENTITY1, entity);
+    StringValue value1 = entity.getValue("str");
+    assertEquals(STR_VALUE, value1);
+    BooleanValue value2 = entity.getValue("bool");
+    assertEquals(BOOL_VALUE, value2);
+    ListValue value3 = entity.getValue("list");
+    assertEquals(LIST_VALUE2, value3);
+    DateTimeValue value4 = entity.getValue("date");
+    assertEquals(DATE_TIME_VALUE, value4);
+    FullEntity value5 = entity.getEntity("partial1");
+    assertEquals(PARTIAL_ENTITY1, value5);
+    assertEquals(5, entity.names().size());
+    assertFalse(entity.contains("bla"));
+  }
+
+  @Test
+  public void testGetArray() {
+    datastore.put(ENTITY3);
+    Iterator result =
+        datastore.fetch(KEY1, Key.builder(KEY1).name("bla").build(), KEY2, KEY3).iterator();
+    assertEquals(ENTITY1, result.next());
+    assertNull(result.next());
+    assertEquals(ENTITY2, result.next());
+    Entity entity3 = result.next();
+    assertEquals(ENTITY3, entity3);
+    assertTrue(entity3.isNull("null"));
+    assertFalse(entity3.getBoolean("bool"));
+    assertEquals(LIST_VALUE2.get(), entity3.getList("list"));
+    FullEntity partial1 = entity3.getEntity("partial1");
+    FullEntity partial2 = entity3.getEntity("partial2");
+    assertEquals(PARTIAL_ENTITY2, partial1);
+    assertEquals(ENTITY2, partial2);
+    assertEquals(ValueType.BOOLEAN, entity3.getValue("bool").type());
+    assertEquals(6, entity3.names().size());
+    assertFalse(entity3.contains("bla"));
+    try {
+      entity3.getString("str");
+      fail("Expecting a failure");
+    } catch (DatastoreException expected) {
+      // expected - no such property
+    }
+    assertFalse(result.hasNext());
+    // TODO(ozarov): construct a test to verify more results
+  }
+
+  @Test
+  public void testAddEntity() {
+    List keys = datastore.fetch(ENTITY1.key(), ENTITY3.key());
+    assertEquals(ENTITY1, keys.get(0));
+    assertNull(keys.get(1));
+    assertEquals(2, keys.size());
+
+    try {
+      datastore.add(ENTITY1);
+      fail("Expecting a failure");
+    } catch (DatastoreException expected) {
+      // expected;
+    }
+
+    List entities = datastore.add(ENTITY3, PARTIAL_ENTITY1, PARTIAL_ENTITY2);
+    assertEquals(ENTITY3, datastore.get(ENTITY3.key()));
+    assertEquals(ENTITY3, entities.get(0));
+    assertEquals(PARTIAL_ENTITY1.properties(), entities.get(1).properties());
+    assertEquals(PARTIAL_ENTITY1.key().ancestors(), entities.get(1).key().ancestors());
+    assertNotNull(datastore.get(entities.get(1).key()));
+    assertEquals(PARTIAL_ENTITY2.properties(), entities.get(2).properties());
+    assertEquals(PARTIAL_ENTITY2.key().ancestors(), entities.get(2).key().ancestors());
+    assertNotNull(datastore.get(entities.get(2).key()));
+  }
+
+
+  @Test
+  public void testUpdate() {
+    List keys = datastore.fetch(ENTITY1.key(), ENTITY3.key());
+    assertEquals(ENTITY1, keys.get(0));
+    assertNull(keys.get(1));
+    assertEquals(2, keys.size());
+
+    try {
+      datastore.update(ENTITY3);
+      fail("Expecting a failure");
+    } catch (DatastoreException expected) {
+      // expected;
+    }
+    datastore.add(ENTITY3);
+    assertEquals(ENTITY3, datastore.get(ENTITY3.key()));
+    Entity entity3 = Entity.builder(ENTITY3).clear().set("bla", new NullValue()).build();
+    assertNotEquals(ENTITY3, entity3);
+    datastore.update(entity3);
+    assertEquals(entity3, datastore.get(ENTITY3.key()));
+  }
+
+  @Test
+  public void testPut() {
+    Iterator keys =
+        datastore.fetch(ENTITY1.key(), ENTITY2.key(), ENTITY3.key()).iterator();
+    assertEquals(ENTITY1, keys.next());
+    assertEquals(ENTITY2, keys.next());
+    assertNull(keys.next());
+    assertFalse(keys.hasNext());
+
+    Entity entity2 = Entity.builder(ENTITY2).clear().set("bla", new NullValue()).build();
+    assertNotEquals(ENTITY2, entity2);
+    datastore.put(ENTITY3, ENTITY1, entity2);
+    keys = datastore.fetch(ENTITY1.key(), ENTITY2.key(), ENTITY3.key()).iterator();
+    assertEquals(ENTITY1, keys.next());
+    assertEquals(entity2, keys.next());
+    assertEquals(ENTITY3, keys.next());
+    assertFalse(keys.hasNext());
+  }
+
+  @Test
+  public void testDelete() {
+    Iterator keys =
+        datastore.fetch(ENTITY1.key(), ENTITY2.key(), ENTITY3.key()).iterator();
+    assertEquals(ENTITY1, keys.next());
+    assertEquals(ENTITY2, keys.next());
+    assertNull(keys.next());
+    assertFalse(keys.hasNext());
+    datastore.delete(ENTITY1.key(), ENTITY2.key(), ENTITY3.key());
+    keys = datastore.fetch(ENTITY1.key(), ENTITY2.key(), ENTITY3.key()).iterator();
+    assertNull(keys.next());
+    assertNull(keys.next());
+    assertNull(keys.next());
+    assertFalse(keys.hasNext());
+  }
+
+  @Test
+  public void testKeyFactory() {
+    KeyFactory keyFactory = datastore.newKeyFactory().kind(KIND1);
+    assertEquals(INCOMPLETE_KEY1, keyFactory.newKey());
+    assertEquals(IncompleteKey.builder(INCOMPLETE_KEY1).kind(KIND2).build(),
+        datastore.newKeyFactory().kind(KIND2).newKey());
+    assertEquals(KEY1, keyFactory.newKey("name"));
+    assertEquals(Key.builder(KEY1).id(2).build(), keyFactory.newKey(2));
+  }
+
+  @Test
+  public void testRetires() throws Exception {
+    DatastoreV1.LookupRequest requestPb =
+        DatastoreV1.LookupRequest.newBuilder().addKey(KEY1.toPb()).build();
+    DatastoreV1.LookupResponse responsePb = DatastoreV1.LookupResponse.newBuilder()
+        .addFound(EntityResult.newBuilder().setEntity(ENTITY1.toPb())).build();
+    DatastoreRpcFactory rpcFactoryMock = EasyMock.createStrictMock(DatastoreRpcFactory.class);
+    DatastoreRpc rpcMock = EasyMock.createStrictMock(DatastoreRpc.class);
+    EasyMock.expect(rpcFactoryMock.create(EasyMock.anyObject(DatastoreOptions.class)))
+        .andReturn(rpcMock);
+    EasyMock.expect(rpcMock.lookup(requestPb))
+        .andThrow(new DatastoreRpc.DatastoreRpcException(Reason.UNAVAILABLE))
+        .andReturn(responsePb);
+    EasyMock.replay(rpcFactoryMock, rpcMock);
+    DatastoreOptions options = this.options.toBuilder()
+        .retryParams(RetryParams.getDefaultInstance())
+        .serviceRpcFactory(rpcFactoryMock)
+        .build();
+    Datastore datastore = DatastoreFactory.instance().get(options);
+    Entity entity = datastore.get(KEY1);
+    assertEquals(ENTITY1, entity);
+    EasyMock.verify(rpcFactoryMock, rpcMock);
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DateTimeTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DateTimeTest.java
new file mode 100644
index 000000000000..a7131e04e89c
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DateTimeTest.java
@@ -0,0 +1 @@
+/*
 * Copyright 2015 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.gcloud.datastore;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

import java.util.Calendar;

public class DateTimeTest {

  @Test
  public void testTimestampMicroseconds() throws Exception {
    Calendar cal = Calendar.getInstance();
    DateTime date = DateTime.copyFrom(cal);
    assertEquals(cal.getTimeInMillis() * 1000, date.timestampMicroseconds());
  }

  @Test
  public void testTimestampMillis() throws Exception {
    Calendar cal = Calendar.getInstance();
    DateTime date = DateTime.copyFrom(cal);
    assertEquals(cal.getTimeInMillis(), date.timestampMillis());
  }

  @Test
  public void testToDate() throws Exception {
    Calendar cal = Calendar.getInstance();
    DateTime date = DateTime.copyFrom(cal);
    assertEquals(cal.getTime(), date.toDate());
  }

  @Test
  public void testToCalendar() throws Exception {
    Calendar cal = Calendar.getInstance();
    DateTime date = DateTime.copyFrom(cal);
    assertEquals(cal, date.toCalendar());
  }

  @Test
  public void testNow() throws Exception {
    Calendar cal1 = Calendar.getInstance();
    DateTime now = DateTime.now();
    Calendar cal2 = Calendar.getInstance();
    assertTrue(now.timestampMillis() >= cal1.getTimeInMillis());
    assertTrue(now.timestampMillis() <= cal2.getTimeInMillis());
  }

  @Test
  public void testCopyFrom() throws Exception {
    Calendar cal = Calendar.getInstance();
    DateTime date1 = DateTime.copyFrom(cal);
    DateTime date2 = DateTime.copyFrom(cal.getTime());
    cal.add(Calendar.DATE, 1);
    DateTime date3 = DateTime.copyFrom(cal.getTime());
    assertEquals(date1, date2);
    assertNotEquals(date1, date3);
  }
}

\ No newline at end of file
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DateTimeValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DateTimeValueTest.java
new file mode 100644
index 000000000000..d7fef2ca69b9
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DateTimeValueTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class DateTimeValueTest {
+
+  private static final DateTime CONTENT = DateTime.now();
+
+  @Test
+  public void testToBuilder() throws Exception {
+    DateTimeValue value = DateTimeValue.of(CONTENT);
+    assertEquals(value, value.toBuilder().build());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testOf() throws Exception {
+    DateTimeValue value = DateTimeValue.of(CONTENT);
+    assertEquals(CONTENT, value.get());
+    assertFalse(value.hasIndexed());
+    assertFalse(value.hasMeaning());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testBuilder() throws Exception {
+    DateTimeValue.Builder builder = DateTimeValue.builder(CONTENT);
+    DateTimeValue value = builder.meaning(1).indexed(false).build();
+    assertEquals(CONTENT, value.get());
+    assertTrue(value.hasMeaning());
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertTrue(value.hasIndexed());
+    assertFalse(value.indexed());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DoubleValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DoubleValueTest.java
new file mode 100644
index 000000000000..fa39511a45de
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/DoubleValueTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class DoubleValueTest {
+
+  private static final Double CONTENT = 1.25;
+
+  @Test
+  public void testToBuilder() throws Exception {
+    DoubleValue value = DoubleValue.of(CONTENT);
+    assertEquals(value, value.toBuilder().build());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testOf() throws Exception {
+    DoubleValue value = DoubleValue.of(CONTENT);
+    assertEquals(CONTENT, value.get());
+    assertFalse(value.hasIndexed());
+    assertFalse(value.hasMeaning());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testBuilder() throws Exception {
+    DoubleValue.Builder builder = DoubleValue.builder(CONTENT);
+    DoubleValue value = builder.meaning(1).indexed(false).build();
+    assertEquals(CONTENT, value.get());
+    assertTrue(value.hasMeaning());
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertTrue(value.hasIndexed());
+    assertFalse(value.indexed());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/EntityTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/EntityTest.java
new file mode 100644
index 000000000000..bb6d1a6eab73
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/EntityTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class EntityTest {
+
+  private static final Key KEY1 = Key.builder("ds1", "k1", "n1").build();
+  private static final Key KEY2 = Key.builder("ds1", "k2", 1).build();
+  private static final IncompleteKey INCOMPLETE_KEY = IncompleteKey.builder("ds1", "k2").build();
+  private static final Entity ENTITY = Entity.builder(KEY1).set("foo", "bar").build();
+  private static final FullEntity INCOMPLETE_ENTITY =
+      Entity.builder(INCOMPLETE_KEY).set("a", "b").build();
+
+  @Test
+  public void testEntity() throws Exception {
+    assertTrue(ENTITY.hasKey());
+    assertEquals(KEY1, ENTITY.key());
+    assertEquals("bar", ENTITY.getString("foo"));
+  }
+
+  @Test
+  public void testCopyFrom() throws Exception {
+    Entity.Builder builder = Entity.builder(ENTITY);
+    assertEquals(ENTITY, builder.build());
+    Entity entity = builder.key(KEY2).build();
+    assertNotEquals(ENTITY, entity);
+    assertEquals(KEY2, entity.key());
+    assertEquals(ENTITY.properties(), entity.properties());
+  }
+
+  @Test
+  public void testCopyFromIncompleteEntity() throws Exception {
+    Entity.Builder builder = Entity.builder(KEY2, INCOMPLETE_ENTITY);
+    Entity entity = builder.build();
+    assertNotEquals(INCOMPLETE_ENTITY, entity);
+    assertEquals(INCOMPLETE_ENTITY.properties(), entity.properties());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/EntityValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/EntityValueTest.java
new file mode 100644
index 000000000000..cd1f7af38067
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/EntityValueTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class EntityValueTest {
+
+  private static final Key KEY = Key.builder("ds", "kind", 1).build();
+  private static final Entity CONTENT = Entity.builder(KEY).set("FOO", "BAR").build();
+
+  @Test
+  public void testToBuilder() throws Exception {
+    EntityValue value = EntityValue.of(CONTENT);
+    assertEquals(value, value.toBuilder().build());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testOf() throws Exception {
+    EntityValue value = EntityValue.of(CONTENT);
+    assertEquals(CONTENT, value.get());
+    assertTrue(value.hasIndexed());
+    assertFalse(value.indexed());
+    assertFalse(value.hasMeaning());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testIndexedNotAllowed() {
+    EntityValue.builder(CONTENT).indexed(true);
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testBuilder() throws Exception {
+    EntityValue.Builder builder = EntityValue.builder(CONTENT);
+    EntityValue value = builder.meaning(1).indexed(false).build();
+    assertEquals(CONTENT, value.get());
+    assertTrue(value.hasMeaning());
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertTrue(value.hasIndexed());
+    assertFalse(value.indexed());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/FullEntityTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/FullEntityTest.java
new file mode 100644
index 000000000000..1d62c7a6dfae
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/FullEntityTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class FullEntityTest {
+
+  private static final Key KEY1 = Key.builder("ds1", "k1", "n1").build();
+  private static final Key KEY2 = Key.builder("ds1", "k2", 1).build();
+  private static final IncompleteKey INCOMPLETE_KEY = IncompleteKey.builder("ds1", "k2").build();
+  private static final Entity ENTITY = Entity.builder(KEY1).set("foo", "bar").build();
+  private static final FullEntity COMPLETE_ENTITY1 = ENTITY;
+  private static final FullEntity COMPLETE_ENTITY2 =
+      FullEntity.builder(KEY2).set("foo", "bar").build();
+  private static final FullEntity INCOMPLETE_ENTITY =
+      Entity.builder(INCOMPLETE_KEY).set("a", "b").build();
+
+  @Test
+  public void testFullEntity() throws Exception {
+    assertTrue(COMPLETE_ENTITY1.hasKey());
+    assertEquals(KEY1, COMPLETE_ENTITY1.key());
+    assertEquals("bar", COMPLETE_ENTITY1.getString("foo"));
+
+    assertTrue(COMPLETE_ENTITY2.hasKey());
+    assertEquals(KEY2, COMPLETE_ENTITY2.key());
+    assertEquals("bar", COMPLETE_ENTITY2.getString("foo"));
+  }
+
+  @Test
+  public void testNoKey() throws Exception {
+    FullEntity entity = FullEntity.builder().set("foo", "bar").build();
+    assertFalse(entity.hasKey());
+    assertNull(entity.key());
+    assertEquals("bar", entity.getString("foo"));
+
+    entity = FullEntity.builder((IncompleteKey) null).build();
+    assertFalse(entity.hasKey());
+    assertNull(entity.key());
+  }
+
+  @Test
+  public void testCopyFrom() throws Exception {
+    FullEntity.Builder builder1 = FullEntity.builder(ENTITY);
+    assertEquals(ENTITY, builder1.build());
+
+    builder1 = FullEntity.builder(COMPLETE_ENTITY1);
+    assertEquals(COMPLETE_ENTITY1, builder1.build());
+
+    FullEntity.Builder builder2 = FullEntity.builder(INCOMPLETE_ENTITY);
+    assertEquals(INCOMPLETE_ENTITY, builder2.build());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/IncompleteKeyTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/IncompleteKeyTest.java
new file mode 100644
index 000000000000..7edbf133d330
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/IncompleteKeyTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class IncompleteKeyTest {
+
+  @Test
+  public void testBuilders() throws Exception {
+    IncompleteKey pk1 = IncompleteKey.builder("ds", "kind1").build();
+    assertEquals("ds", pk1.projectId());
+    assertEquals("kind1", pk1.kind());
+    assertTrue(pk1.ancestors().isEmpty());
+
+    Key parent = Key.builder("ds", "kind2", 10).build();
+    IncompleteKey pk2 = IncompleteKey.builder(parent, "kind3").build();
+    assertEquals("ds", pk2.projectId());
+    assertEquals("kind3", pk2.kind());
+    assertEquals(parent.path(), pk2.ancestors());
+
+    assertEquals(pk2, IncompleteKey.builder(pk2).build());
+    IncompleteKey pk3 = IncompleteKey.builder(pk2).kind("kind4").build();
+    assertEquals("ds", pk3.projectId());
+    assertEquals("kind4", pk3.kind());
+    assertEquals(parent.path(), pk3.ancestors());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/KeyFactoryTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/KeyFactoryTest.java
new file mode 100644
index 000000000000..92851bd87efe
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/KeyFactoryTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Iterator;
+
+public class KeyFactoryTest {
+
+  private static final String PROJECT_ID = "projectid";
+
+  private KeyFactory keyFactory;
+
+  @Before
+  public void setUp() {
+    keyFactory = new KeyFactory(PROJECT_ID).kind("k");
+  }
+
+  @Test
+  public void testReset() {
+    IncompleteKey key =
+        keyFactory.projectId("ds1").namespace("ns1").ancestors(PathElement.of("p", 1)).build();
+    assertEquals("k", key.kind());
+    assertEquals("ds1", key.projectId());
+    assertEquals("ns1", key.namespace());
+    assertEquals(1, key.ancestors().size());
+
+    keyFactory.reset();
+    try {
+      keyFactory.newKey(1);
+    } catch (NullPointerException ex) {
+      assertEquals("kind must not be null", ex.getMessage());
+    }
+    keyFactory.kind("k1");
+    key = keyFactory.newKey();
+    assertEquals("k1", key.kind());
+    assertEquals(PROJECT_ID, key.projectId());
+    assertNull(key.namespace());
+    assertTrue(key.ancestors().isEmpty());
+
+    keyFactory = new KeyFactory(PROJECT_ID, "ns1").kind("k");
+    key = keyFactory.newKey();
+    assertEquals(PROJECT_ID, key.projectId());
+    assertEquals("ns1", key.namespace());
+    key = keyFactory.projectId("bla1").namespace("bla2").build();
+    assertEquals("bla1", key.projectId());
+    assertEquals("bla2", key.namespace());
+    keyFactory.reset().kind("kind");
+    key = keyFactory.newKey();
+    assertEquals(PROJECT_ID, key.projectId());
+    assertEquals("ns1", key.namespace());
+    assertEquals("kind", key.kind());
+  }
+
+  @Test
+  public void testNewKey() throws Exception {
+    Key key = keyFactory.newKey(1);
+    verifyKey(key, 1L, null);
+    key = keyFactory.newKey("n");
+    verifyKey(key, "n", null);
+    PathElement p1 = PathElement.of("k1", "n");
+    PathElement p2 = PathElement.of("k2", 10);
+    key = keyFactory.namespace("ns").ancestors(p1, p2).newKey("k3");
+    verifyKey(key, "k3", "ns", p1, p2);
+  }
+
+  @Test
+  public void testNewIncompleteKey() throws Exception {
+    IncompleteKey key = keyFactory.newKey();
+    verifyIncompleteKey(key, null);
+    PathElement p1 = PathElement.of("k1", "n");
+    PathElement p2 = PathElement.of("k2", 10);
+    key = keyFactory.namespace("ns").ancestors(p1, p2).newKey();
+    verifyIncompleteKey(key, "ns", p1, p2);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testNewIncompleteWithNoKind() {
+    new KeyFactory(PROJECT_ID).build();
+  }
+
+  private void verifyKey(Key key, String name, String namespace, PathElement... ancestors) {
+    assertEquals(name, key.name());
+    verifyIncompleteKey(key, namespace, ancestors);
+  }
+
+  private void verifyKey(Key key, Long id, String namespace, PathElement... ancestors) {
+    assertEquals(id, key.id());
+    verifyIncompleteKey(key, namespace, ancestors);
+  }
+
+  private void verifyIncompleteKey(IncompleteKey key, String namespace, PathElement... ancestors) {
+    assertEquals("k", key.kind());
+    assertEquals(PROJECT_ID, key.projectId());
+    assertEquals(namespace, key.namespace());
+    assertEquals(ancestors.length, key.ancestors().size());
+    Iterator iter = key.ancestors().iterator();
+    for (PathElement ancestor : ancestors) {
+      assertEquals(ancestor, iter.next());
+    }
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/KeyTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/KeyTest.java
new file mode 100644
index 000000000000..1fdcc5394e7e
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/KeyTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class KeyTest {
+
+  @Test
+  public void testHasId() throws Exception {
+    Key.Builder builder = Key.builder("d", "k", 10);
+    Key key = builder.build();
+    assertTrue(key.hasId());
+    key = builder.name("bla").build();
+    assertFalse(key.hasId());
+  }
+
+  @Test
+  public void testId() throws Exception {
+    Key.Builder builder = Key.builder("d", "k", 10);
+    Key key = builder.build();
+    assertEquals(Long.valueOf(10), key.id());
+    key = builder.id(100).build();
+    assertEquals(Long.valueOf(100), key.id());
+  }
+
+  @Test
+  public void testHasName() throws Exception {
+    Key.Builder builder = Key.builder("d", "k", "n");
+    Key key = builder.build();
+    assertTrue(key.hasName());
+    key = builder.id(1).build();
+    assertFalse(key.hasName());
+  }
+
+  @Test
+  public void testName() throws Exception {
+    Key.Builder builder = Key.builder("d", "k", "n");
+    Key key = builder.build();
+    assertEquals("n", key.name());
+    key = builder.name("o").build();
+    assertEquals("o", key.name());
+  }
+
+  @Test
+  public void testNameOrId() throws Exception {
+    Key.Builder builder = Key.builder("d", "k", "n");
+    Key key = builder.build();
+    assertEquals("n", key.nameOrId());
+    key = builder.id(1).build();
+    assertEquals(Long.valueOf(1), key.nameOrId());
+  }
+
+  @Test
+  public void testToAndFromUrlSafe() throws Exception {
+    Key key = Key.builder("d", "k", "n").build();
+    String urlSafe = key.toUrlSafe();
+    Key copy = Key.fromUrlSafe(urlSafe);
+    assertEquals(key, copy);
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/KeyValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/KeyValueTest.java
new file mode 100644
index 000000000000..131a80462a62
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/KeyValueTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class KeyValueTest {
+
+  private static final Key CONTENT = Key.builder("ds", "kind", 1).build();
+
+  @Test
+  public void testToBuilder() throws Exception {
+    KeyValue value = KeyValue.of(CONTENT);
+    assertEquals(value, value.toBuilder().build());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testOf() throws Exception {
+    KeyValue value = KeyValue.of(CONTENT);
+    assertEquals(CONTENT, value.get());
+    assertFalse(value.hasIndexed());
+    assertFalse(value.hasMeaning());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testBuilder() throws Exception {
+    KeyValue.Builder builder = KeyValue.builder(CONTENT);
+    KeyValue value = builder.meaning(1).indexed(false).build();
+    assertEquals(CONTENT, value.get());
+    assertTrue(value.hasMeaning());
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertTrue(value.hasIndexed());
+    assertFalse(value.indexed());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/ListValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/ListValueTest.java
new file mode 100644
index 000000000000..36e3571d49ac
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/ListValueTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+
+import java.util.List;
+
+public class ListValueTest {
+
+  private static final List> CONTENT = ImmutableList.of(NullValue.of(), StringValue.of("foo"));
+
+  @Test
+  public void testToBuilder() throws Exception {
+    ListValue value = ListValue.of(CONTENT);
+    assertEquals(value, value.toBuilder().build());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testOf() throws Exception {
+    ListValue value = ListValue.of(CONTENT);
+    assertEquals(CONTENT, value.get());
+    assertFalse(value.hasIndexed());
+    assertFalse(value.hasMeaning());
+  }
+
+  @Test(expected = DatastoreException.class)
+  public void testIndexedCannotBeSpecified() {
+    ListValue.builder().indexed(false);
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testBuilder() throws Exception {
+    ListValue.Builder builder = ListValue.builder().set(CONTENT);
+    ListValue value = builder.meaning(1).build();
+    assertEquals(CONTENT, value.get());
+    assertTrue(value.hasMeaning());
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertFalse(value.hasIndexed());
+
+    builder = ListValue.builder();
+    for (Value v : CONTENT) {
+      builder.addValue(v);
+    }
+    assertEquals(CONTENT, builder.build().get());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/LocalGcdHelper.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/LocalGcdHelper.java
new file mode 100644
index 000000000000..7ed9a4e830cf
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/LocalGcdHelper.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Strings;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Locale;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * Utility to start and stop local Google Cloud Datastore process.
+ */
+public class LocalGcdHelper {
+
+  private final String projectId;
+  private Path gcdPath;
+  private ProcessStreamReader processReader;
+
+  public static final String DEFAULT_PROJECT_ID = "projectid1";
+  public static final int PORT = 8080;
+  private static final String GCD = "gcd-v1beta2-rev1-2.1.2b";
+  private static final String GCD_FILENAME = GCD + ".zip";
+  private static final String MD5_CHECKSUM = "d84384cdfa8658e1204f4f8be51300e8";
+  private static final URL GCD_URL;
+
+  static {
+    try {
+      GCD_URL = new URL("http://storage.googleapis.com/gcd/tools/" + GCD_FILENAME);
+    } catch (MalformedURLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static class ProcessStreamReader extends Thread {
+
+    private final Process process;
+    private final BufferedReader reader;
+
+    ProcessStreamReader(Process process, String blockUntil) throws IOException {
+      super("Local GCD InputStream reader");
+      setDaemon(true);
+      this.process = process;
+      reader =  new BufferedReader(new InputStreamReader(process.getInputStream()));
+      if (!Strings.isNullOrEmpty(blockUntil)) {
+        String line;
+        do {
+          line = reader.readLine();
+        } while (line != null && !line.contains(blockUntil));
+      }
+    }
+
+    void terminate() throws InterruptedException, IOException {
+      process.destroy();
+      process.waitFor();
+      reader.close();
+    }
+
+    @Override
+    public void run() {
+      try {
+        while (reader.readLine() != null) {
+          // consume line
+        }
+      } catch (IOException e) {
+        // ignore
+      }
+    }
+
+    public static ProcessStreamReader start(Process process, String blockUntil) throws IOException {
+      ProcessStreamReader thread = new ProcessStreamReader(process, blockUntil);
+      thread.start();
+      return thread;
+    }
+  }
+
+  public LocalGcdHelper(String projectId) {
+    this.projectId = projectId;
+  }
+
+  /**
+   * Starts the local datastore for the specific project.
+   *
+   * This will unzip the gcd tool, create the project and start it.
+   * All content is written to a temporary directory that will be deleted when
+   * {@link #stop()} is called or when the program terminates) to make sure that no left-over
+   * data from prior runs is used.
+   */
+  public void start() throws IOException, InterruptedException {
+    // send a quick request in case we have a hanging process from a previous run
+    sendQuitRequest();
+    // Each run is associated with its own folder that is deleted once test completes.
+    gcdPath = Files.createTempDirectory("gcd");
+    File gcdFolder = gcdPath.toFile();
+    gcdFolder.deleteOnExit();
+
+    // check if we already have a local copy of the gcd utility and download it if not.
+    File gcdZipFile = new File(System.getProperty("java.io.tmpdir"), GCD_FILENAME);
+    if (!gcdZipFile.exists() || !MD5_CHECKSUM.equals(md5(gcdZipFile))) {
+      ReadableByteChannel rbc = Channels.newChannel(GCD_URL.openStream());
+      FileOutputStream fos = new FileOutputStream(gcdZipFile);
+      fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+      fos.close();
+    }
+    // unzip the gcd
+    try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(gcdZipFile))) {
+      ZipEntry entry = zipIn.getNextEntry();
+      while (entry != null) {
+        File filePath = new File(gcdFolder, entry.getName());
+        if (!entry.isDirectory()) {
+          extractFile(zipIn, filePath);
+        } else {
+          filePath.mkdir();
+        }
+        zipIn.closeEntry();
+        entry = zipIn.getNextEntry();
+      }
+    }
+    // cleanup any possible data for the same project
+    File datasetFolder = new File(gcdFolder, GCD + '/' + projectId);
+    deleteRecurse(datasetFolder.toPath());
+
+    // create the datastore for the project
+    ProcessBuilder processBuilder = new ProcessBuilder()
+        .redirectError(ProcessBuilder.Redirect.INHERIT)
+        .directory(new File(gcdFolder, GCD));
+    if (isWindows()) {
+      processBuilder.command("cmd", "/C", "gcd.cmd", "create", "-p", projectId, projectId);
+      processBuilder.redirectOutput(new File("NULL:"));
+    } else {
+      processBuilder.redirectOutput(new File("/dev/null"));
+      processBuilder.command("bash", "gcd.sh", "create", "-p", projectId, projectId);
+    }
+
+    Process temp = processBuilder.start();
+    temp.waitFor();
+
+    // start the datastore for the project
+    processBuilder = new ProcessBuilder()
+        .directory(new File(gcdFolder, GCD))
+        .redirectErrorStream(true);
+    if (isWindows()) {
+      processBuilder.command("cmd", "/C", "gcd.cmd", "start", "--testing",
+          "--allow_remote_shutdown", projectId);
+    } else {
+      processBuilder.command("bash", "gcd.sh", "start", "--testing", "--allow_remote_shutdown",
+          projectId);
+    }
+    temp = processBuilder.start();
+    processReader = ProcessStreamReader.start(temp, "Dev App Server is now running");
+  }
+
+  private static String md5(File gcdZipFile) throws IOException {
+    try {
+      MessageDigest md5 = MessageDigest.getInstance("MD5");
+      try (InputStream is = new BufferedInputStream(new FileInputStream(gcdZipFile))) {
+        byte[] bytes = new byte[4 * 1024 * 1024];
+        int len;
+        while ((len = is.read(bytes)) >= 0) {
+          md5.update(bytes, 0, len);
+        }
+      }
+      return String.format("%032x",new BigInteger(1, md5.digest()));
+    } catch (NoSuchAlgorithmException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private static boolean isWindows() {
+    return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).indexOf("windows") > -1;
+  }
+
+  private static void extractFile(ZipInputStream zipIn, File filePath) throws IOException {
+    try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) {
+      byte[] bytesIn = new byte[1024];
+      int read;
+      while ((read = zipIn.read(bytesIn)) != -1) {
+        bos.write(bytesIn, 0, read);
+      }
+    }
+  }
+
+  public static void sendQuitRequest() {
+    try {
+      URL url = new URL("http", "localhost", PORT, "/_ah/admin/quit");
+      HttpURLConnection con = (HttpURLConnection) url.openConnection();
+      con.setRequestMethod("POST");
+      con.setDoOutput(true);
+      con.setDoInput(true);
+      OutputStream out = con.getOutputStream();
+      out.write("".getBytes());
+      out.flush();
+      InputStream in = con.getInputStream();
+      while (in.read() != -1) {
+        // consume input
+      }
+    } catch (IOException ignore) {
+      // ignore
+    }
+  }
+
+  public void stop() throws IOException, InterruptedException {
+    sendQuitRequest();
+    if (processReader != null) {
+      processReader.terminate();
+    }
+    if (gcdPath != null) {
+      deleteRecurse(gcdPath);
+    }
+  }
+
+  private static void deleteRecurse(Path path) throws IOException {
+    if (path == null || !Files.exists(path)) {
+      return;
+    }
+    Files.walkFileTree(path, new SimpleFileVisitor() {
+
+      @Override
+      public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+        Files.delete(dir);
+        return FileVisitResult.CONTINUE;
+      }
+
+      @Override
+      public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+        Files.delete(file);
+        return FileVisitResult.CONTINUE;
+      }
+    });
+  }
+
+  public static LocalGcdHelper start(String projectId) throws IOException, InterruptedException {
+    LocalGcdHelper helper = new LocalGcdHelper(projectId);
+    helper.start();
+    return helper;
+  }
+
+  public static void main(String... args) throws IOException, InterruptedException {
+    if (args.length == 1) {
+      switch (args[0]) {
+        case "START":
+          if (!isActive(DEFAULT_PROJECT_ID)) {
+            LocalGcdHelper helper = start(DEFAULT_PROJECT_ID);
+            try (FileWriter writer = new FileWriter(".local_gcd_helper")) {
+              writer.write(helper.gcdPath.toAbsolutePath().toString());
+            }
+          }
+          return;
+        case "STOP":
+          sendQuitRequest();
+          File file = new File(".local_gcd_helper");
+          if (file.exists()) {
+            try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+              String path = reader.readLine();
+              deleteRecurse(Paths.get(path));
+            }
+            file.delete();
+          }
+          return;
+        default:
+          break;
+      }
+    }
+    throw new RuntimeException("expecting only START | STOP");
+  }
+
+  public static boolean isActive(String projectId) {
+    try {
+      StringBuilder urlBuilder = new StringBuilder("http://localhost:").append(PORT);
+      urlBuilder.append("/datastore/v1beta2/datasets/").append(projectId).append("/lookup");
+      URL url = new URL(urlBuilder.toString());
+      try (BufferedReader reader =
+               new BufferedReader(new InputStreamReader(url.openStream(), UTF_8))) {
+        return "Valid RPC".equals(reader.readLine());
+      }
+    } catch (IOException ignore) {
+      // assume not active
+      return false;
+    }
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/LongValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/LongValueTest.java
new file mode 100644
index 000000000000..c4c899785d68
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/LongValueTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class LongValueTest {
+
+  private static final Long CONTENT = 125L;
+
+  @Test
+  public void testToBuilder() throws Exception {
+    LongValue value = LongValue.of(CONTENT);
+    assertEquals(value, value.toBuilder().build());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testOf() throws Exception {
+    LongValue value = LongValue.of(CONTENT);
+    assertEquals(CONTENT, value.get());
+    assertFalse(value.hasIndexed());
+    assertFalse(value.hasMeaning());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testBuilder() throws Exception {
+    LongValue.Builder builder = LongValue.builder(CONTENT);
+    LongValue value = builder.meaning(1).indexed(false).build();
+    assertEquals(CONTENT, value.get());
+    assertTrue(value.hasMeaning());
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertTrue(value.hasIndexed());
+    assertFalse(value.indexed());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/NullValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/NullValueTest.java
new file mode 100644
index 000000000000..a42fdaf0229f
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/NullValueTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class NullValueTest {
+
+  @Test
+  public void testToBuilder() throws Exception {
+    NullValue value = NullValue.of();
+    assertEquals(value, value.toBuilder().build());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testOf() throws Exception {
+    NullValue value = NullValue.of();
+    assertNull(value.get());
+    assertFalse(value.hasIndexed());
+    assertFalse(value.hasMeaning());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testBuilder() throws Exception {
+    NullValue.Builder builder = NullValue.builder();
+    NullValue value = builder.meaning(1).indexed(false).build();
+    assertNull(value.get());
+    assertTrue(value.hasMeaning());
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertTrue(value.hasIndexed());
+    assertFalse(value.indexed());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/PathElementTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/PathElementTest.java
new file mode 100644
index 000000000000..393521ff08b9
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/PathElementTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class PathElementTest {
+
+  private static final PathElement PE_1 = PathElement.of("k1");
+  private static final PathElement PE_2 = PathElement.of("k2", "n");
+  private static final PathElement PE_3 = PathElement.of("k3", 1);
+
+  @Test
+  public void testKind() throws Exception {
+    assertEquals("k1", PE_1.kind());
+    assertEquals("k2", PE_2.kind());
+    assertEquals("k3", PE_3.kind());
+  }
+
+  @Test
+  public void testHasId() throws Exception {
+    assertFalse(PE_1.hasId());
+    assertFalse(PE_2.hasId());
+    assertTrue(PE_3.hasId());
+  }
+
+  @Test
+  public void testId() throws Exception {
+    assertNull(PE_1.id());
+    assertNull(PE_2.id());
+    assertEquals(Long.valueOf(1), PE_3.id());
+  }
+
+  @Test
+  public void testHasName() throws Exception {
+    assertFalse(PE_1.hasName());
+    assertTrue(PE_2.hasName());
+    assertFalse(PE_3.hasName());
+  }
+
+  @Test
+  public void testName() throws Exception {
+    assertNull(PE_1.name());
+    assertEquals("n", PE_2.name());
+    assertNull(PE_3.name());
+  }
+
+  @Test
+  public void testNameOrId() throws Exception {
+    assertNull(PE_1.nameOrId());
+    assertEquals("n", PE_2.nameOrId());
+    assertEquals(Long.valueOf(1), PE_3.nameOrId());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/ProjectionEntityTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/ProjectionEntityTest.java
new file mode 100644
index 000000000000..0262fb04b89d
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/ProjectionEntityTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class ProjectionEntityTest {
+
+  private static final Key KEY = Key.builder("ds1", "k1", "n1").build();
+  private static final StringValue STRING_INDEX_VALUE = StringValue.builder("foo").meaning(18).build();
+  private static final BlobValue BLOB_VALUE = BlobValue.of(Blob.copyFrom(new byte[]{1}));
+  private static final DateTimeValue DATE_TIME_VALUE = DateTimeValue.of(DateTime.now());
+  private static final LongValue LONG_INDEX_VALUE =
+      LongValue.builder(DATE_TIME_VALUE.get().timestampMicroseconds()).meaning(18).build();
+  private static final ProjectionEntity ENTITY1 =
+      new ProjectionEntity.Builder().key(KEY).set("a", "b").build();
+  private static final ProjectionEntity ENTITY2 = new ProjectionEntity.Builder()
+      .set("a", STRING_INDEX_VALUE)
+      .set("b", BLOB_VALUE)
+      .set("c", DATE_TIME_VALUE)
+      .set("d", LONG_INDEX_VALUE)
+      .build();
+
+  @Test
+  public void testHasKey() throws Exception {
+    assertTrue(ENTITY1.hasKey());
+    assertFalse(ENTITY2.hasKey());
+  }
+
+  @Test
+  public void testKey() throws Exception {
+    assertEquals(KEY, ENTITY1.key());
+    assertNull(ENTITY2.key());
+  }
+
+  @Test
+  public void testGetBlob() throws Exception {
+    assertArrayEquals(STRING_INDEX_VALUE.get().getBytes(), ENTITY2.getBlob("a").toByteArray());
+    assertEquals(BLOB_VALUE.get(), ENTITY2.getBlob("b"));
+  }
+
+  @Test
+  public void testGetDateTime() throws Exception {
+    assertEquals(DATE_TIME_VALUE.get(), ENTITY2.getDateTime("c"));
+    assertEquals(DATE_TIME_VALUE.get(), ENTITY2.getDateTime("d"));
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/RawValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/RawValueTest.java
new file mode 100644
index 000000000000..4d63bc89bacb
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/RawValueTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.api.services.datastore.DatastoreV1;
+
+import org.junit.Test;
+
+public class RawValueTest {
+
+  private static final DatastoreV1.Value CONTENT = StringValue.of("hello").toPb();
+
+  @Test
+  public void testToBuilder() throws Exception {
+    RawValue value = RawValue.of(CONTENT);
+    assertEquals(value, value.toBuilder().build());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testOf() throws Exception {
+    RawValue value = RawValue.of(CONTENT);
+    assertEquals(CONTENT, value.get());
+    assertFalse(value.hasIndexed());
+    assertFalse(value.hasMeaning());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testBuilder() throws Exception {
+    RawValue.Builder builder = RawValue.builder(CONTENT);
+    RawValue value = builder.meaning(1).indexed(false).build();
+    assertEquals(CONTENT, value.get());
+    assertTrue(value.hasMeaning());
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertTrue(value.hasIndexed());
+    assertFalse(value.indexed());
+  }
+}
diff --git a/src/test/java/com/google/gcloud/datastore/SerializationTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/SerializationTest.java
similarity index 52%
rename from src/test/java/com/google/gcloud/datastore/SerializationTest.java
rename to gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/SerializationTest.java
index 636ab831c8ac..9574f1e246d2 100644
--- a/src/test/java/com/google/gcloud/datastore/SerializationTest.java
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/SerializationTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -7,49 +23,52 @@
 import com.google.api.services.datastore.DatastoreV1;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Multimap;
+import com.google.gcloud.AuthCredentials;
+import com.google.gcloud.RetryParams;
 import com.google.gcloud.datastore.StructuredQuery.CompositeFilter;
 import com.google.gcloud.datastore.StructuredQuery.OrderBy;
 import com.google.gcloud.datastore.StructuredQuery.Projection;
 import com.google.gcloud.datastore.StructuredQuery.PropertyFilter;
-import com.google.gcloud.datastore.Value.Type;
 
 import org.junit.Test;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 
 public class SerializationTest {
 
-  private static final PartialKey INCOMPLETE_KEY1 =
-      PartialKey.builder("ds", "k").ancestors(PathElement.of("p", 1)).build();
+  private static final IncompleteKey INCOMPLETE_KEY1 =
+      IncompleteKey.builder("ds", "k").ancestors(PathElement.of("p", 1)).build();
   private static final Key KEY1 = Key.builder("ds", "k", "n").build();
-  private static final PartialKey INCOMPLETE_KEY2 =
-      PartialKey.builder(KEY1, "v").ancestors(PathElement.of("p", 1)).build();
+  private static final IncompleteKey INCOMPLETE_KEY2 =
+      IncompleteKey.builder(KEY1, "v").ancestors(PathElement.of("p", 1)).build();
   private static final Key KEY2 = Key.builder(KEY1, "v", 2).build();
   private static final DateTime DATE_TIME1 = DateTime.now();
   private static final Blob BLOB1 = Blob.copyFrom(UTF_8.encode("hello world"));
   private static final Cursor CURSOR1 = Cursor.copyFrom(new byte[] {1,2});
-  private static final Cursor CURSOR2 = Cursor.copyFrom(new byte[] {10});
+  private static final Cursor CURSOR2 = Cursor.copyFrom(new byte[]{10});
   private static final Query GQL1 =
-      GqlQuery.builder("select * from kind1 where name = @name and age > @1")
+      Query.gqlQueryBuilder("select * from kind1 where name = @name and age > @1")
       .setBinding("name", "name1")
       .addBinding(20)
       .namespace("ns1")
       .build();
   private static final Query GQL2 =
-      GqlQuery.builder(Query.Type.FULL, "select * from kind1 where name = @name and age > @1")
+      Query.gqlQueryBuilder(Query.ResultType.ENTITY, "select * from kind1 where name = @name and age > @1")
       .setBinding("name", "name1")
       .addBinding(20)
       .namespace("ns1")
       .build();
-  private static final Query QUERY1 = StructuredQuery.builder().kind("kind1").build();
-  private static final Query QUERY2 = StructuredQuery.keyOnlyBuilder()
+  private static final Query QUERY1 =
+      Query.entityQueryBuilder().kind("kind1").build();
+  private static final Query QUERY2 = Query.keyQueryBuilder()
       .kind("k")
       .filter(PropertyFilter.eq("p1", "hello"))
       .build();
-  private static final Query QUERY3 = StructuredQuery.projectionBuilder()
+  private static final Query QUERY3 = Query.projectionEntityQueryBuilder()
       .kind("k")
       .namespace("ns1")
       .projection(Projection.property("p"))
@@ -80,15 +99,13 @@ public class SerializationTest {
       .set("p3", LongValue.builder(100).indexed(false).meaning(100).build())
       .set("blob", BLOB1)
       .build();
-  private static final PartialEntity EMBEDDED_ENTITY1 = ENTITY1;
-  private static final PartialEntity EMBEDDED_ENTITY2 = ENTITY2;
-  private static final PartialEntity EMBEDDED_ENTITY3 = PartialEntity.builder(INCOMPLETE_KEY1)
+  private static final FullEntity EMBEDDED_ENTITY = Entity.builder(INCOMPLETE_KEY1)
       .set("p1", STRING_VALUE)
       .set("p2", LongValue.builder(100).indexed(false).meaning(100).build())
       .build();
-  private static final EntityValue EMBEDDED_ENTITY_VALUE1 = EntityValue.of(EMBEDDED_ENTITY1);
-  private static final EntityValue EMBEDDED_ENTITY_VALUE2 = EntityValue.of(EMBEDDED_ENTITY2);
-  private static final EntityValue EMBEDDED_ENTITY_VALUE3 = EntityValue.of(EMBEDDED_ENTITY3);
+  private static final EntityValue EMBEDDED_ENTITY_VALUE1 = EntityValue.of(ENTITY1);
+  private static final EntityValue EMBEDDED_ENTITY_VALUE2 = EntityValue.of(ENTITY2);
+  private static final EntityValue EMBEDDED_ENTITY_VALUE3 = EntityValue.of(EMBEDDED_ENTITY);
   private static final ListValue LIST_VALUE = ListValue.builder()
       .addValue(NULL_VALUE)
       .addValue(STRING_VALUE)
@@ -97,26 +114,47 @@ public class SerializationTest {
   private static final ProjectionEntity PROJECTION_ENTITY = ProjectionEntity.fromPb(ENTITY1.toPb());
 
   @SuppressWarnings("rawtypes")
-  private Multimap typeToValues = ImmutableMultimap.builder()
-      .put(Type.NULL, NULL_VALUE)
-      .put(Type.KEY, KEY_VALUE)
-      .put(Type.STRING, STRING_VALUE)
-      .putAll(Type.ENTITY, EMBEDDED_ENTITY_VALUE1, EMBEDDED_ENTITY_VALUE2,
+  private static final Multimap TYPE_TO_VALUES =
+      ImmutableMultimap.builder()
+      .put(ValueType.NULL, NULL_VALUE)
+      .put(ValueType.KEY, KEY_VALUE)
+      .put(ValueType.STRING, STRING_VALUE)
+      .putAll(ValueType.ENTITY, EMBEDDED_ENTITY_VALUE1, EMBEDDED_ENTITY_VALUE2,
           EMBEDDED_ENTITY_VALUE3)
-      .put(Type.LIST, LIST_VALUE)
-      .put(Type.LONG, LONG_VALUE)
-      .put(Type.DOUBLE, DOUBLE_VALUE)
-      .put(Type.BOOLEAN, BOOLEAN_VALUE)
-      .put(Type.DATE_TIME, DATE_AND_TIME_VALUE)
-      .put(Type.BLOB, BLOB_VALUE)
-      .put(Type.RAW_VALUE, RAW_VALUE)
+      .put(ValueType.LIST, LIST_VALUE)
+      .put(ValueType.LONG, LONG_VALUE)
+      .put(ValueType.DOUBLE, DOUBLE_VALUE)
+      .put(ValueType.BOOLEAN, BOOLEAN_VALUE)
+      .put(ValueType.DATE_TIME, DATE_AND_TIME_VALUE)
+      .put(ValueType.BLOB, BLOB_VALUE)
+      .put(ValueType.RAW_VALUE, RAW_VALUE)
       .build();
 
+  @Test
+  public void testServiceOptions() throws Exception {
+    DatastoreOptions options = DatastoreOptions.builder()
+        .authCredentials(AuthCredentials.createForAppEngine())
+        .normalizeDataset(false)
+        .projectId("ds1")
+        .build();
+    DatastoreOptions serializedCopy = serializeAndDeserialize(options);
+    assertEquals(options, serializedCopy);
+
+    options = options.toBuilder()
+        .namespace("ns1")
+        .retryParams(RetryParams.getDefaultInstance())
+        .authCredentials(AuthCredentials.noCredentials())
+        .force(true)
+        .build();
+    serializedCopy = serializeAndDeserialize(options);
+    assertEquals(options, serializedCopy);
+  }
+
   @Test
   public void testValues() throws Exception {
-    for (Type type : Type.values()) {
-      for (Value value : typeToValues.get(type)) {
-        Value copy = serialiazeAndDeserialize(value);
+    for (ValueType valueType : ValueType.values()) {
+      for (Value value : TYPE_TO_VALUES.get(valueType)) {
+        Value copy = serializeAndDeserialize(value);
         assertEquals(value, value);
         assertEquals(value, copy);
         assertNotSame(value, copy);
@@ -128,11 +166,11 @@ public void testValues() throws Exception {
 
   @Test
   public void testTypes() throws Exception {
-    Object[] types = { KEY1, KEY2, INCOMPLETE_KEY1, INCOMPLETE_KEY2, ENTITY1, ENTITY2,
-        ENTITY3, EMBEDDED_ENTITY1, EMBEDDED_ENTITY2, EMBEDDED_ENTITY3, PROJECTION_ENTITY,
-        DATE_TIME1, BLOB1, CURSOR1, GQL1, GQL2, QUERY1, QUERY2, QUERY3};
-    for (Object obj : types) {
-      Object copy = serialiazeAndDeserialize(obj);
+    Serializable[] types = { KEY1, KEY2, INCOMPLETE_KEY1, INCOMPLETE_KEY2, ENTITY1, ENTITY2,
+        ENTITY3, EMBEDDED_ENTITY, PROJECTION_ENTITY, DATE_TIME1, BLOB1, CURSOR1, GQL1, GQL2,
+        QUERY1, QUERY2, QUERY3};
+    for (Serializable obj : types) {
+      Object copy = serializeAndDeserialize(obj);
       assertEquals(obj, obj);
       assertEquals(obj, copy);
       assertNotSame(obj, copy);
@@ -141,15 +179,15 @@ public void testTypes() throws Exception {
   }
 
   @SuppressWarnings("unchecked")
-  private  T serialiazeAndDeserialize(T obj) throws Exception {
-    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-    try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
-      objectOutputStream.writeObject(obj);
+  private  T serializeAndDeserialize(T obj)
+      throws IOException, ClassNotFoundException {
+    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+    try (ObjectOutputStream output = new ObjectOutputStream(bytes)) {
+      output.writeObject(obj);
     }
-    byte[] bytes = byteArrayOutputStream.toByteArray();
-    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
-    try (ObjectInputStream in = new ObjectInputStream(byteArrayInputStream)) {
-      return (T) in.readObject();
+    try (ObjectInputStream input =
+        new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
+      return (T) input.readObject();
     }
   }
 }
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/StringValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/StringValueTest.java
new file mode 100644
index 000000000000..a2cacd6574aa
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/StringValueTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class StringValueTest {
+
+  private static final String CONTENT = "hello world";
+
+  @Test
+  public void testToBuilder() throws Exception {
+    StringValue value = StringValue.of(CONTENT);
+    assertEquals(value, value.toBuilder().build());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testOf() throws Exception {
+    StringValue value = StringValue.of(CONTENT);
+    assertEquals(CONTENT, value.get());
+    assertFalse(value.hasIndexed());
+    assertFalse(value.hasMeaning());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testBuilder() throws Exception {
+    StringValue.Builder builder = StringValue.builder(CONTENT);
+    StringValue value = builder.meaning(1).indexed(false).build();
+    assertEquals(CONTENT, value.get());
+    assertTrue(value.hasMeaning());
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertTrue(value.hasIndexed());
+    assertFalse(value.indexed());
+  }
+}
diff --git a/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/ValueTest.java b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/ValueTest.java
new file mode 100644
index 000000000000..bbfb790b69a2
--- /dev/null
+++ b/gcloud-java-datastore/src/test/java/com/google/gcloud/datastore/ValueTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2015 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.gcloud.datastore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.primitives.Primitives;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+public class ValueTest {
+
+  private static final Key KEY = Key.builder("ds", "kind", 1).build();
+  private static final Blob BLOB = Blob.copyFrom(new byte[]{});
+  private static final DateTime DATE_TIME = DateTime.now();
+  private static final Entity ENTITY = Entity.builder(KEY).set("FOO", "BAR").build();
+  private static final NullValue NULL_VALUE = NullValue.of();
+  private static final StringValue STRING_VALUE = StringValue.of("hello");
+  private static final RawValue RAW_VALUE = RawValue.of(STRING_VALUE.toPb());
+  private static final ImmutableMap TYPES = ImmutableMap.builder()
+      .put(ValueType.NULL, new Object[] {NullValue.class, NULL_VALUE.get()})
+      .put(ValueType.KEY, new Object[] {KeyValue.class, KEY})
+      .put(ValueType.BLOB, new Object[] {BlobValue.class, BLOB})
+      .put(ValueType.BOOLEAN, new Object[] {BooleanValue.class, Boolean.TRUE})
+      .put(ValueType.DATE_TIME, new Object[] {DateTimeValue.class, DATE_TIME})
+      .put(ValueType.DOUBLE, new Object[] {DoubleValue.class, 1.25D})
+      .put(ValueType.ENTITY, new Object[] {EntityValue.class, ENTITY})
+      .put(ValueType.LIST,
+          new Object[] {ListValue.class, ImmutableList.of(NULL_VALUE, STRING_VALUE, RAW_VALUE)})
+      .put(ValueType.LONG, new Object[] {LongValue.class, 123L})
+      .put(ValueType.RAW_VALUE, new Object[] {RawValue.class, RAW_VALUE.get()})
+      .put(ValueType.STRING, new Object[] {StringValue.class, STRING_VALUE.get()})
+      .build();
+
+  private ImmutableMap> typeToValue;
+
+  private class TestBuilder extends Value.BaseBuilder, TestBuilder> {
+    TestBuilder() {
+      super(ValueType.LIST);
+    }
+
+    @Override
+    public Value build() {
+      return new Value(this) {
+
+        @Override
+        public TestBuilder toBuilder() {
+          return new TestBuilder().mergeFrom(this);
+        }
+      };
+    }
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    ImmutableMap.Builder> builder = ImmutableMap.builder();
+    for (ValueType valueType : ValueType.values()) {
+      Object[] values = TYPES.get(valueType);
+      Class> valueClass = (Class>) values[0];
+      Object value = values[1];
+      if (value == null) {
+        Method method = valueClass.getMethod("of");
+        builder.put(valueType, (Value) method.invoke(null));
+      } else {
+        boolean found = false;
+        for (Method method : valueClass.getDeclaredMethods()) {
+          if (method.getName().equals("of")) {
+            Class paramType = method.getParameterTypes()[0];
+            if (paramType.isPrimitive()) {
+              paramType = Primitives.wrap(paramType);
+            }
+            if (paramType.isAssignableFrom(value.getClass())) {
+              builder.put(valueType, (Value) method.invoke(null, value));
+              found = true;
+              break;
+            }
+          }
+        }
+        assertTrue("Could not find an of method for " + valueClass, found);
+      }
+    }
+    typeToValue = builder.build();
+  }
+
+  @Test
+  public void testType() throws Exception {
+    for (Map.Entry> entry : typeToValue.entrySet()) {
+      assertEquals(entry.getKey(), entry.getValue().type());
+    }
+  }
+
+  @Test
+  public void testHasIndexed() throws Exception {
+    for (Map.Entry> entry : typeToValue.entrySet()) {
+      ValueType valueType = entry.getKey();
+      Boolean indexed = entry.getValue().hasIndexed();
+      switch (valueType) {
+        case ENTITY:
+          assertTrue(indexed);
+          break;
+        default:
+          assertFalse(indexed);
+          break;
+      }
+    }
+
+    TestBuilder builder = new TestBuilder();
+    assertFalse(builder.build().hasIndexed());
+    assertTrue(builder.indexed(false).build().hasIndexed());
+    assertTrue(builder.indexed(true).build().hasIndexed());
+  }
+
+  @Test
+  public void testIndexed() throws Exception {
+    for (Map.Entry> entry : typeToValue.entrySet()) {
+      ValueType valueType = entry.getKey();
+      Boolean indexed = entry.getValue().indexed();
+      switch (valueType) {
+        case ENTITY:
+          assertFalse(indexed);
+          break;
+        default:
+          assertNull(indexed);
+          break;
+      }
+    }
+
+    TestBuilder builder = new TestBuilder();
+    assertNull(builder.build().indexed());
+    assertFalse(builder.indexed(false).build().indexed());
+    assertTrue(builder.indexed(true).build().indexed());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testHasMeaning() throws Exception {
+    for (Value value: typeToValue.values()) {
+      assertFalse(value.hasMeaning());
+    }
+
+    TestBuilder builder = new TestBuilder();
+    assertTrue(builder.meaning(10).build().hasMeaning());
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testMeaning() throws Exception {
+    for (Value value: typeToValue.values()) {
+      assertNull(value.meaning());
+    }
+
+    TestBuilder builder = new TestBuilder();
+    assertEquals(Integer.valueOf(10), builder.meaning(10).build().meaning());
+  }
+
+  @Test
+  public void testGet() throws Exception {
+    for (Map.Entry> entry : typeToValue.entrySet()) {
+      ValueType valueType = entry.getKey();
+      Value value = entry.getValue();
+      assertEquals(TYPES.get(valueType)[1], value.get());
+    }
+
+    TestBuilder builder = new TestBuilder();
+    Set value = Collections.singleton("bla");
+    assertEquals(value, builder.set(value).build().get());
+  }
+
+  @SuppressWarnings({"unchecked", "deprecation"})
+  @Test
+  public void testToBuilder() throws Exception {
+    Set content = Collections.singleton("bla");
+    ValueBuilder builder = new TestBuilder();
+    builder.meaning(1).set(content).indexed(true);
+    Value value = builder.build();
+    builder = value.toBuilder();
+    assertEquals(Integer.valueOf(1), value.meaning());
+    assertTrue(value.hasIndexed());
+    assertTrue(value.indexed());
+    assertEquals(ValueType.LIST, value.type());
+    assertEquals(content, value.get());
+    assertEquals(value, builder.build());
+  }
+}
diff --git a/gcloud-java-examples/README.md b/gcloud-java-examples/README.md
new file mode 100644
index 000000000000..59df6ced388f
--- /dev/null
+++ b/gcloud-java-examples/README.md
@@ -0,0 +1,55 @@
+Google Cloud Java Client Examples
+=================================
+
+Examples for gcloud-java (Java idiomatic client for [Google Cloud Platform][cloud-platform] services).
+
+[![Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-java.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/gcloud-java)
+[![Coverage Status](https://coveralls.io/repos/GoogleCloudPlatform/gcloud-java/badge.svg?branch=master)](https://coveralls.io/r/GoogleCloudPlatform/gcloud-java?branch=master)
+
+-  [Homepage] (https://googlecloudplatform.github.io/gcloud-java/)
+-  [API Documentation] (http://googlecloudplatform.github.io/gcloud-java/apidocs)
+-  [Examples] (http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/examples/package-summary.html)
+
+
+Quickstart
+----------
+Add this to your pom.xml file
+```xml
+
+  com.google.gcloud
+  gcloud-java-examples
+  LATEST
+
+```
+
+
+Contributing
+------------
+
+Contributions to this library are always welcome and highly encouraged.
+
+See [CONTRIBUTING] for more information on how to get started.
+
+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.
+
+License
+-------
+
+Apache 2.0 - See [LICENSE] for more information.
+
+
+[CONTRIBUTING]:https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/CONTRIBUTING.md
+[LICENSE]: https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/LICENSE
+[cloud-platform]: https://cloud.google.com/
diff --git a/gcloud-java-examples/pom.xml b/gcloud-java-examples/pom.xml
new file mode 100644
index 000000000000..1c0357d63635
--- /dev/null
+++ b/gcloud-java-examples/pom.xml
@@ -0,0 +1,35 @@
+
+
+  4.0.0
+  com.google.gcloud
+  gcloud-java-examples
+  jar
+  GCloud Java examples
+  https://github.com/GoogleCloudPlatform/gcloud-java
+  
+    Examples for gcloud-java.
+  
+  
+    com.google.gcloud
+    gcloud-java-pom
+    0.0.5
+  
+  
+    
+      ${project.groupId}
+      gcloud-java
+      ${project.version}
+    
+  
+  
+    
+      
+        org.codehaus.mojo
+        exec-maven-plugin
+        
+          false
+        
+      
+    
+  
+
diff --git a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/DatastoreExample.java b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/DatastoreExample.java
new file mode 100644
index 000000000000..9188117e4327
--- /dev/null
+++ b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/DatastoreExample.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2015 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.gcloud.examples;
+
+import com.google.gcloud.datastore.Datastore;
+import com.google.gcloud.datastore.DatastoreFactory;
+import com.google.gcloud.datastore.DatastoreOptions;
+import com.google.gcloud.datastore.DateTime;
+import com.google.gcloud.datastore.Entity;
+import com.google.gcloud.datastore.FullEntity;
+import com.google.gcloud.datastore.IncompleteKey;
+import com.google.gcloud.datastore.Key;
+import com.google.gcloud.datastore.KeyFactory;
+import com.google.gcloud.datastore.Query;
+import com.google.gcloud.datastore.Query.ResultType;
+import com.google.gcloud.datastore.QueryResults;
+import com.google.gcloud.datastore.StructuredQuery.PropertyFilter;
+import com.google.gcloud.datastore.Transaction;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * An example of using the Google Cloud Datastore.
+ * 

+ * This example adds, display or clear comments for a given user. + *

+ * Steps needed for running the example:

    + *
  1. login using gcloud SDK - {@code gcloud auth login}.
  2. + *
  3. compile using maven - {@code mvn compile}
  4. + *
  5. run using maven - {@code mvn exec:java + * -Dexec.mainClass="com.google.gcloud.examples.DatastoreExample" + * -Dexec.args="[projectId] [user] [delete|display|add comment]"}
  6. + *
+ */ +public class DatastoreExample { + + private static final String USER_KIND = "_DS_EXAMPLE_USER"; + private static final String COMMENT_KIND = "_DS_EXAMPLE_COMMENT"; + private static final String NAMESPACE = "gcloud_java_example"; + private static final String DEFAULT_ACTION = "display"; + private static final Map ACTIONS = new HashMap<>(); + + private interface DatastoreAction { + void run(Transaction tx, Key userKey, String... args); + String getRequiredParams(); + } + + private static class DeleteAction implements DatastoreAction { + @Override + public void run(Transaction tx, Key userKey, String... args) { + Entity user = tx.get(userKey); + if (user == null) { + System.out.println("Nothing to delete, user does not exists."); + return; + } + Query query = Query.keyQueryBuilder() + .namespace(NAMESPACE) + .kind(COMMENT_KIND) + .filter(PropertyFilter.hasAncestor(userKey)) + .build(); + QueryResults comments = tx.run(query); + int count = 0; + while (comments.hasNext()) { + tx.delete(comments.next()); + count++; + } + tx.delete(userKey); + System.out.printf("Deleting user '%s' and %d comment[s].%n", userKey.name(), count); + } + + @Override + public String getRequiredParams() { + return ""; + } + } + + private static class DisplayAction implements DatastoreAction { + @Override + public void run(Transaction tx, Key userKey, String... args) { + Entity user = tx.get(userKey); + if (user == null) { + System.out.println("No comments for '" + userKey.name() + "'."); + return; + } + System.out.printf("User '%s' has %d comment[s].%n", userKey.name(), user.getLong("count")); + // ORDER BY timestamp"; + String gql = "SELECT * FROM " + COMMENT_KIND + " WHERE __key__ HAS ANCESTOR @1"; + Query query = Query.gqlQueryBuilder(ResultType.ENTITY, gql) + .namespace(NAMESPACE) + .addBinding(userKey) + .build(); + QueryResults results = tx.run(query); + // We could have added "ORDER BY timestamp" to the query to avoid the sorting bellow + // but that would require adding an ancestor index for timestamp + // see: https://cloud.google.com/datastore/docs/tools/indexconfig + Map sortedComments = new TreeMap<>(); + while (results.hasNext()) { + Entity result = results.next(); + sortedComments.put(result.getDateTime("timestamp"), result.getString("content")); + } + for (Map.Entry entry : sortedComments.entrySet()) { + System.out.printf("\t%s: %s%n", entry.getKey(), entry.getValue()); + } + } + + @Override + public String getRequiredParams() { + return ""; + } + } + + private static class AddAction implements DatastoreAction { + @Override + public void run(Transaction tx, Key userKey, String... args) { + Entity user = tx.get(userKey); + if (user == null) { + System.out.println("Adding a new user."); + user = Entity.builder(userKey) + .set("count", 1L) + .build(); + tx.add(user); + } else { + user = Entity.builder(user).set("count", user.getLong("count") + 1L).build(); + tx.update(user); + } + String content = "No comment."; + if (args.length > 0) { + StringBuilder stBuilder = new StringBuilder(); + for (String arg : args) { + stBuilder.append(arg).append(' '); + } + stBuilder.setLength(stBuilder.length() - 1); + content = stBuilder.toString(); + } + IncompleteKey commentKey = IncompleteKey.builder(userKey, COMMENT_KIND).build(); + FullEntity comment = FullEntity.builder(commentKey) + .set("content", content) + .set("timestamp", DateTime.now()) + .build(); + tx.addWithDeferredIdAllocation(comment); + System.out.println("Adding a comment to user '" + userKey.name() + "'."); + } + + @Override + public String getRequiredParams() { + return "comment"; + } + } + + static { + ACTIONS.put("delete", new DeleteAction()); + ACTIONS.put("add", new AddAction()); + ACTIONS.put("display", new DisplayAction()); + } + + public static void main(String... args) { + DatastoreAction action = null; + Datastore datastore = null; + Key key = null; + String projectId = args.length > 0 ? args[0] : null; + // If you want to access a local Datastore running via the gcd sdk, do + // DatastoreOptions options = DatastoreOptions.builder() + // .projectId(projectId) + // .namespace(NAMESPACE) + // .host("http://localhost:8080") + // .build(); + DatastoreOptions options = DatastoreOptions.builder() + .projectId(projectId) + .namespace(NAMESPACE) + .build(); + String name = args.length > 1 ? args[1] : System.getProperty("user.name"); + datastore = DatastoreFactory.instance().get(options); + KeyFactory keyFactory = datastore.newKeyFactory().kind(USER_KIND); + key = keyFactory.newKey(name); + String actionName = args.length > 2 ? args[2].toLowerCase() : DEFAULT_ACTION; + action = ACTIONS.get(actionName); + if (action == null) { + StringBuilder actionAndParams = new StringBuilder(); + for (Map.Entry entry : ACTIONS.entrySet()) { + actionAndParams.append(entry.getKey()); + String param = entry.getValue().getRequiredParams(); + if (param != null && !param.isEmpty()) { + actionAndParams.append(' ').append(param); + } + actionAndParams.append('|'); + } + actionAndParams.setLength(actionAndParams.length() - 1); + System.out.printf("Usage: %s [projectId] [user] [%s]%n", + DatastoreExample.class.getSimpleName(), actionAndParams); + return; + } + args = args.length > 3 ? Arrays.copyOfRange(args, 3, args.length): new String []{}; + Transaction tx = datastore.newTransaction(); + try { + action.run(tx, key, args); + tx.commit(); + } finally { + if (tx.active()) { + tx.rollback(); + } + } + } +} diff --git a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/StorageExample.java b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/StorageExample.java new file mode 100644 index 000000000000..26437b995bd2 --- /dev/null +++ b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/StorageExample.java @@ -0,0 +1,591 @@ +/* + * Copyright 2015 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.gcloud.examples; + +import com.google.gcloud.AuthCredentials; +import com.google.gcloud.AuthCredentials.ServiceAccountAuthCredentials; +import com.google.gcloud.RetryParams; +import com.google.gcloud.spi.StorageRpc.Tuple; +import com.google.gcloud.storage.BatchRequest; +import com.google.gcloud.storage.BatchResponse; +import com.google.gcloud.storage.BlobInfo; +import com.google.gcloud.storage.BlobReadChannel; +import com.google.gcloud.storage.BlobWriteChannel; +import com.google.gcloud.storage.BucketInfo; +import com.google.gcloud.storage.Storage; +import com.google.gcloud.storage.Storage.ComposeRequest; +import com.google.gcloud.storage.Storage.CopyRequest; +import com.google.gcloud.storage.Storage.SignUrlOption; +import com.google.gcloud.storage.StorageFactory; +import com.google.gcloud.storage.StorageOptions; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; + +/** + * An example of using the Google Cloud Storage. + *

+ * This example demonstrates a simple/typical storage usage. + *

+ * Steps needed for running the example: + *

    + *
  1. login using gcloud SDK - {@code gcloud auth login}.
  2. + *
  3. compile using maven - {@code mvn compile}
  4. + *
  5. run using maven - + * {@code mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.StorageExample" + * -Dexec.args="[] list []| info [ []]| + * download [local_file]| upload []| + * delete +| cp | + * compose + | update_metadata [key=value]*| + * sign_url "} + *
  6. + *
+ * + * The first parameter is an optional project_id (logged-in project will be used if not supplied). + * Second parameter is a Storage operation (list, delete, compose,...) to demonstrate the its + * usage. Any other arguments are specific to the operation. + * See each action's run method for the specific Storage interaction. + */ +public class StorageExample { + + private static final Map ACTIONS = new HashMap<>(); + + private static abstract class StorageAction { + + abstract void run(Storage storage, T request) throws Exception; + + abstract T parse(String... args) throws Exception; + + protected String params() { + return ""; + } + } + + private static abstract class BlobAction extends StorageAction { + + @Override + BlobInfo parse(String... args) { + if (args.length != 2) { + throw new IllegalArgumentException(); + } + return BlobInfo.of(args[0], args[1]); + } + + @Override + public String params() { + return " "; + } + } + + private static abstract class BlobsAction extends StorageAction { + + @Override + BlobInfo[] parse(String... args) { + if (args.length < 2) { + throw new IllegalArgumentException(); + } + BlobInfo[] blobInfos = new BlobInfo[args.length - 1]; + for (int i = 1; i < args.length; i++) { + blobInfos[i - 1] = BlobInfo.of(args[0], args[i]); + } + return blobInfos; + } + + @Override + public String params() { + return " +"; + } + } + + /** + * This class demonstrates how to retrieve Bucket or Blob metadata. + * If more than one blob is supplied a Batch operation would be used to get all blobs metadata + * in a single RPC. + * + * @see Objects: get + */ + private static class InfoAction extends BlobsAction { + @Override + public void run(Storage storage, BlobInfo... blobInfos) { + if (blobInfos.length == 1) { + if (blobInfos[0].name().isEmpty()) { + // get Bucket + BucketInfo bucketInfo = storage.get(blobInfos[0].bucket()); + System.out.println("Bucket info: " + bucketInfo); + } else { + // get Blob + BlobInfo blobInfo = storage.get(blobInfos[0].bucket(), blobInfos[0].name()); + System.out.println("Blob info: " + blobInfo); + } + } else { + // use batch to get multiple blobs. + BatchRequest.Builder batch = BatchRequest.builder(); + for (BlobInfo blobInfo : blobInfos) { + batch.get(blobInfo.bucket(), blobInfo.name()); + } + BatchResponse response = storage.apply(batch.build()); + for (BatchResponse.Result result : response.gets()) { + System.out.println(result.get()); + } + } + } + + @Override + BlobInfo[] parse(String... args) { + if (args.length < 2) { + return new BlobInfo[] {BlobInfo.of(args[0], "")}; + } + return super.parse(args); + } + + @Override + public String params() { + return " [+]"; + } + } + + /** + * This class demonstrates how to delete a blob. + * If more than one blob is supplied a Batch operation would be used to delete all requested + * blobs in a single RPC. + * + * @see Objects: delete + */ + private static class DeleteAction extends BlobsAction { + @Override + public void run(Storage storage, BlobInfo... blobInfos) { + if (blobInfos.length == 1) { + boolean wasDeleted = storage.delete(blobInfos[0].bucket(), blobInfos[0].name()); + if (wasDeleted) { + System.out.println("Blob " + blobInfos[0] + " was deleted"); + } + } else { + // use batch operation + BatchRequest.Builder batch = BatchRequest.builder(); + for (BlobInfo blobInfo : blobInfos) { + batch.delete(blobInfo.bucket(), blobInfo.name()); + } + int index = 0; + BatchResponse response = storage.apply(batch.build()); + for (BatchResponse.Result result : response.deletes()) { + if (result.get()) { + // request order is maintained + System.out.println("Blob " + blobInfos[index] + " was deleted"); + } + index++; + } + } + } + } + + /** + * This class demonstrates how to list buckets or a bucket's blobs. + * + * @see Objects: list + */ + private static class ListAction extends StorageAction { + + @Override + String parse(String... args) { + if (args.length == 0) { + return null; + } + if (args.length == 1) { + return args[0]; + } + throw new IllegalArgumentException(); + } + + @Override + public void run(Storage storage, String bucket) { + if (bucket == null) { + // list buckets + for (BucketInfo b : storage.list()) { + System.out.println(b); + } + } else { + // list a bucket's blobs + for (BlobInfo b : storage.list(bucket)) { + System.out.println(b); + } + } + } + + @Override + public String params() { + return "[]"; + } + } + + /** + * This class demonstrates how to create a new Blob or to update its content. + * + * @see Objects: insert + */ + private static class UploadAction extends StorageAction> { + @Override + public void run(Storage storage, Tuple tuple) throws Exception { + run(storage, tuple.x(), tuple.y()); + } + + private void run(Storage storage, Path uploadFrom, BlobInfo blobInfo) throws IOException { + if (Files.size(uploadFrom) > 1_000_000) { + // When content is not available or large (1MB or more) it is recommended + // to write it in chunks via the blob's channel writer. + try (BlobWriteChannel writer = storage.writer(blobInfo)) { + byte[] buffer = new byte[1024]; + try (InputStream input = Files.newInputStream(uploadFrom)) { + int limit; + while ((limit = input.read(buffer)) >= 0) { + try { + writer.write(ByteBuffer.wrap(buffer, 0, limit)); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + } + } else { + byte[] bytes = Files.readAllBytes(uploadFrom); + // create the blob in one request. + storage.create(blobInfo, bytes); + } + System.out.println("Blob was created"); + } + + @Override + Tuple parse(String... args) throws IOException { + if (args.length < 2 || args.length > 3) { + throw new IllegalArgumentException(); + } + Path path = Paths.get(args[0]); + String contentType = Files.probeContentType(path); + String blob = args.length < 3 ? path.getFileName().toString() : args[2]; + return Tuple.of(path, BlobInfo.builder(args[1], blob).contentType(contentType).build()); + } + + @Override + public String params() { + return " []"; + } + } + + /** + * This class demonstrates how read a blob's content. + * The example will dump the content to a local file if one was given or write + * it to stdout otherwise. + * + * @see Objects: get + */ + private static class DownloadAction extends StorageAction> { + + @Override + public void run(Storage storage, Tuple tuple) throws IOException { + run(storage, tuple.x().bucket(), tuple.x().name(), tuple.y()); + } + + private void run(Storage storage, String bucket, String blobName, Path downloadTo) + throws IOException { + BlobInfo blobInfo = storage.get(bucket, blobName); + if (blobInfo == null) { + System.out.println("No such object"); + return; + } + PrintStream writeTo = System.out; + if (downloadTo != null) { + writeTo = new PrintStream(new FileOutputStream(downloadTo.toFile())); + } + if (blobInfo.size() < 1_000_000) { + // Blob is small read all its content in one request + byte[] content = storage.readAllBytes(blobInfo.bucket(), blobInfo.name()); + writeTo.write(content); + } else { + // When Blob size is big or unknown use the blob's channel reader. + try (BlobReadChannel reader = storage.reader(blobInfo.bucket(), blobInfo.name())) { + WritableByteChannel channel = Channels.newChannel(writeTo); + ByteBuffer bytes = ByteBuffer.allocate(64 * 1024); + while (reader.read(bytes) > 0) { + bytes.flip(); + channel.write(bytes); + bytes.clear(); + } + } + } + if (downloadTo == null) { + writeTo.println(); + } else { + writeTo.close(); + } + } + + @Override + Tuple parse(String... args) { + if (args.length < 2 || args.length > 3) { + throw new IllegalArgumentException(); + } + Path path; + if (args.length > 2) { + path = Paths.get(args[2]); + if (Files.isDirectory(path)) { + path = path.resolve(Paths.get(args[1]).getFileName()); + } + } else { + path = null; + } + return Tuple.of(BlobInfo.of(args[0], args[1]), path); + } + + @Override + public String params() { + return " [local_file]"; + } + } + + /** + * This class demonstrates how to use the copy command. + * + * @see Objects: copy + */ + private static class CopyAction extends StorageAction { + @Override + public void run(Storage storage, CopyRequest request) { + BlobInfo copiedBlobInfo = storage.copy(request); + System.out.println("Copied " + copiedBlobInfo); + } + + @Override + CopyRequest parse(String... args) { + if (args.length != 4) { + throw new IllegalArgumentException(); + } + return CopyRequest.of(args[0], args[1], BlobInfo.of(args[2], args[3])); + } + + @Override + public String params() { + return " "; + } + } + + /** + * This class demonstrates how to use the compose command. + * + * @see Objects: compose + */ + private static class ComposeAction extends StorageAction { + @Override + public void run(Storage storage, ComposeRequest request) { + BlobInfo composedBlobInfo = storage.compose(request); + System.out.println("Composed " + composedBlobInfo); + } + + @Override + ComposeRequest parse(String... args) { + if (args.length < 3) { + throw new IllegalArgumentException(); + } + ComposeRequest.Builder request = ComposeRequest.builder(); + request.target(BlobInfo.of(args[0], args[args.length - 1])); + for (int i = 1; i < args.length - 1; i++) { + request.addSource(args[i]); + } + return request.build(); + } + + @Override + public String params() { + return " + "; + } + } + + /** + * This class demonstrates how to update a blob's metadata. + * + * @see Objects: update + */ + private static class UpdateMetadataAction extends StorageAction>> { + + @Override + public void run(Storage storage, Tuple> tuple) + throws IOException { + run(storage, tuple.x().bucket(), tuple.x().name(), tuple.y()); + } + + private void run(Storage storage, String bucket, String blobName, + Map metadata) { + BlobInfo blobInfo = storage.get(bucket, blobName); + if (blobInfo == null) { + System.out.println("No such object"); + return; + } + blobInfo = storage.update(blobInfo.toBuilder().metadata(metadata).build()); + System.out.println("Updated " + blobInfo); + } + + @Override + Tuple> parse(String... args) { + if (args.length < 2) { + throw new IllegalArgumentException(); + } + BlobInfo blobInfo = BlobInfo.of(args[0], args[1]); + Map metadata = new HashMap<>(); + for (int i = 2; i < args.length; i++) { + int idx = args[i].indexOf('='); + if (idx < 0) { + metadata.put(args[i], ""); + } else { + metadata.put(args[i].substring(0, idx), args[i].substring(idx + 1)); + } + } + return Tuple.of(blobInfo, metadata); + } + + @Override + public String params() { + return " [local_file]"; + } + } + + /** + * This class demonstrates how to sign a url. + * URL will be valid for 1 day. + * + * @see Signed URLs + */ + private static class SignUrlAction extends + StorageAction> { + + private static final char[] PASSWORD = "notasecret".toCharArray(); + + @Override + public void run(Storage storage, Tuple tuple) + throws Exception { + run(storage, tuple.x(), tuple.y()); + } + + private void run(Storage storage, ServiceAccountAuthCredentials cred, BlobInfo blobInfo) + throws IOException { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, 1); + long expiration = cal.getTimeInMillis() / 1000; + System.out.println("Signed URL: " + + storage.signUrl(blobInfo, expiration, SignUrlOption.serviceAccount(cred))); + } + + @Override + Tuple parse(String... args) + throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, + UnrecoverableKeyException { + if (args.length != 4) { + throw new IllegalArgumentException(); + } + KeyStore keystore = KeyStore.getInstance("PKCS12"); + keystore.load(Files.newInputStream(Paths.get(args[0])), PASSWORD); + PrivateKey privateKey = (PrivateKey) keystore.getKey("privatekey", PASSWORD); + ServiceAccountAuthCredentials cred = AuthCredentials.createFor(args[1], privateKey); + return Tuple.of(cred, BlobInfo.of(args[2], args[3])); + } + + @Override + public String params() { + return " "; + } + } + + static { + ACTIONS.put("info", new InfoAction()); + ACTIONS.put("delete", new DeleteAction()); + ACTIONS.put("list", new ListAction()); + ACTIONS.put("upload", new UploadAction()); + ACTIONS.put("download", new DownloadAction()); + ACTIONS.put("cp", new CopyAction()); + ACTIONS.put("compose", new ComposeAction()); + ACTIONS.put("update_metadata", new UpdateMetadataAction()); + ACTIONS.put("sign_url", new SignUrlAction()); + } + + public static void printUsage() { + StringBuilder actionAndParams = new StringBuilder(); + for (Map.Entry entry : ACTIONS.entrySet()) { + actionAndParams.append("\n\t").append(entry.getKey()); + + String param = entry.getValue().params(); + if (param != null && !param.isEmpty()) { + actionAndParams.append(' ').append(param); + } + } + System.out.printf("Usage: %s [] operation *%s%n", + StorageExample.class.getSimpleName(), actionAndParams); + } + + @SuppressWarnings("unchecked") + public static void main(String... args) throws Exception { + if (args.length < 1) { + System.out.println("Missing required project id and action"); + printUsage(); + return; + } + StorageOptions.Builder optionsBuilder = + StorageOptions.builder().retryParams(RetryParams.getDefaultInstance()); + StorageAction action; + if (args.length >= 2 && !ACTIONS.containsKey(args[0])) { + optionsBuilder.projectId(args[0]); + action = ACTIONS.get(args[1]); + args = Arrays.copyOfRange(args, 2, args.length); + } else { + action = ACTIONS.get(args[0]); + args = Arrays.copyOfRange(args, 1, args.length); + } + if (action == null) { + System.out.println("Unrecognized action."); + printUsage(); + return; + } + Storage storage = StorageFactory.instance().get(optionsBuilder.build()); + Object request; + try { + request = action.parse(args); + } catch (IllegalArgumentException ex) { + System.out.println("Invalid input for action '" + args[1] + "'"); + System.out.println("Expected: " + action.params()); + return; + } catch (Exception ex) { + System.out.println("Failed to parse request."); + ex.printStackTrace(); + return; + } + action.run(storage, request); + } +} diff --git a/gcloud-java-storage/README.md b/gcloud-java-storage/README.md new file mode 100644 index 000000000000..aa090743f5ce --- /dev/null +++ b/gcloud-java-storage/README.md @@ -0,0 +1,63 @@ +Google Cloud Java Client +========================== + +Java idiomatic client for [Google Cloud Platform][cloud-platform] services. + +[![Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-java.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/gcloud-java) +[![Coverage Status](https://coveralls.io/repos/GoogleCloudPlatform/gcloud-java/badge.svg?branch=master)](https://coveralls.io/r/GoogleCloudPlatform/gcloud-java?branch=master) + +- [Homepage] (https://googlecloudplatform.github.io/gcloud-java/) +- [API Documentation] (http://googlecloudplatform.github.io/gcloud-java/apidocs) +- [Examples] (http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/examples/package-summary.html) + +This client supports the [Google Cloud Storage] (https://cloud.google.com/storage/) + +> Note: This client is a work-in-progress, and may occasionally +> make backwards-incompatible changes. + +Quickstart +---------- +Add this to your pom.xml file +```xml + + com.google.gcloud + gcloud-java-storage + LATEST + +``` + + +Contributing +------------ + +Contributions to this library are always welcome and highly encouraged. + +See [CONTRIBUTING] for more information on how to get started. + +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. + +License +------- + +Apache 2.0 - See [LICENSE] for more information. + + +[CONTRIBUTING]:https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/CONTRIBUTING.md +[LICENSE]: https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/LICENSE +[cloud-platform]: https://cloud.google.com/ + +[cloud-storage]: https://cloud.google.com/storage/ +[cloud-storage-docs]: https://cloud.google.com/storage/docs/overview +[cloud-storage-create-bucket]: https://cloud.google.com/storage/docs/cloud-console#_creatingbuckets diff --git a/gcloud-java-storage/pom.xml b/gcloud-java-storage/pom.xml new file mode 100644 index 000000000000..11cc0f02c6ac --- /dev/null +++ b/gcloud-java-storage/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + com.google.gcloud + gcloud-java-storage + jar + GCloud Java storage + https://github.com/GoogleCloudPlatform/gcloud-java + + Java idiomatic client for Google Cloud Storage. + + + com.google.gcloud + gcloud-java-pom + 0.0.5 + + + + ${project.groupId} + gcloud-java-core + ${project.version} + + + com.google.apis + google-api-services-storage + v1-rev33-1.20.0 + compile + + + junit + junit + 4.12 + test + + + org.easymock + easymock + 3.3 + test + + + diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java new file mode 100644 index 000000000000..89dfb4c6c414 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java @@ -0,0 +1,525 @@ +/* + * Copyright 2015 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.gcloud.spi; + +import static com.google.gcloud.spi.StorageRpc.Option.DELIMITER; +import static com.google.gcloud.spi.StorageRpc.Option.IF_GENERATION_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_GENERATION_NOT_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_METAGENERATION_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_METAGENERATION_NOT_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_SOURCE_GENERATION_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_SOURCE_GENERATION_NOT_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_SOURCE_METAGENERATION_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_SOURCE_METAGENERATION_NOT_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.MAX_RESULTS; +import static com.google.gcloud.spi.StorageRpc.Option.PAGE_TOKEN; +import static com.google.gcloud.spi.StorageRpc.Option.PREDEFINED_ACL; +import static com.google.gcloud.spi.StorageRpc.Option.PREDEFINED_DEFAULT_OBJECT_ACL; +import static com.google.gcloud.spi.StorageRpc.Option.PREFIX; +import static com.google.gcloud.spi.StorageRpc.Option.VERSIONS; + +import com.google.api.client.googleapis.batch.json.JsonBatchCallback; +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.googleapis.media.MediaHttpDownloader; +import com.google.api.client.http.ByteArrayContent; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson.JacksonFactory; +import com.google.api.services.storage.Storage; +import com.google.api.services.storage.Storage.Objects.Get; +import com.google.api.services.storage.Storage.Objects.Insert; +import com.google.api.services.storage.model.Bucket; +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.Objects; +import com.google.api.services.storage.model.StorageObject; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.gcloud.storage.StorageException; +import com.google.gcloud.storage.StorageOptions; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class DefaultStorageRpc implements StorageRpc { + + public static final String DEFAULT_PROJECTION = "full"; + private final StorageOptions options; + private final Storage storage; + + // see: https://cloud.google.com/storage/docs/concepts-techniques#practices + private static final Set RETRYABLE_CODES = ImmutableSet.of(504, 503, 502, 500, 408); + + public DefaultStorageRpc(StorageOptions options) { + HttpTransport transport = options.httpTransportFactory().create(); + HttpRequestInitializer initializer = options.httpRequestInitializer(); + this.options = options; + storage = new Storage.Builder(transport, new JacksonFactory(), initializer) + .setRootUrl(options.host()) + .setApplicationName("gcloud-java") + .build(); + } + + private static StorageException translate(IOException exception) { + StorageException translated; + if (exception instanceof GoogleJsonResponseException) { + translated = translate(((GoogleJsonResponseException) exception).getDetails()); + } else { + translated = new StorageException(0, exception.getMessage(), false); + } + translated.initCause(exception); + return translated; + } + + private static StorageException translate(GoogleJsonError exception) { + boolean retryable = RETRYABLE_CODES.contains(exception.getCode()) + || "InternalError".equals(exception.getMessage()); + return new StorageException(exception.getCode(), exception.getMessage(), retryable); + } + + @Override + public Bucket create(Bucket bucket, Map options) throws StorageException { + try { + return storage.buckets() + .insert(this.options.projectId(), bucket) + .setProjection(DEFAULT_PROJECTION) + .setPredefinedAcl(PREDEFINED_ACL.getString(options)) + .setPredefinedDefaultObjectAcl(PREDEFINED_DEFAULT_OBJECT_ACL.getString(options)) + .execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public StorageObject create(StorageObject storageObject, final byte[] content, + Map options) throws StorageException { + try { + return storage.objects() + .insert(storageObject.getBucket(), storageObject, + new ByteArrayContent(storageObject.getContentType(), content)) + .setProjection(DEFAULT_PROJECTION) + .setPredefinedAcl(PREDEFINED_ACL.getString(options)) + .setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(options)) + .setIfMetagenerationNotMatch(IF_METAGENERATION_NOT_MATCH.getLong(options)) + .setIfGenerationMatch(IF_GENERATION_MATCH.getLong(options)) + .setIfGenerationNotMatch(IF_GENERATION_NOT_MATCH.getLong(options)) + .execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public Tuple> list(Map options) { + try { + Buckets buckets = storage.buckets() + .list(this.options.projectId()) + .setProjection(DEFAULT_PROJECTION) + .setPrefix(PREFIX.getString(options)) + .setMaxResults(MAX_RESULTS.getLong(options)) + .setPageToken(PAGE_TOKEN.getString(options)) + .execute(); + return Tuple.>of(buckets.getNextPageToken(), buckets.getItems()); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public Tuple> list(String bucket, Map options) { + try { + Objects objects = storage.objects() + .list(bucket) + .setProjection(DEFAULT_PROJECTION) + .setVersions(VERSIONS.getBoolean(options)) + .setDelimiter(DELIMITER.getString(options)) + .setPrefix(PREFIX.getString(options)) + .setMaxResults(MAX_RESULTS.getLong(options)) + .setPageToken(PAGE_TOKEN.getString(options)) + .execute(); + return Tuple.>of( + objects.getNextPageToken(), objects.getItems()); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public Bucket get(Bucket bucket, Map options) { + try { + return storage.buckets() + .get(bucket.getName()) + .setProjection(DEFAULT_PROJECTION) + .setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(options)) + .setIfMetagenerationNotMatch(IF_METAGENERATION_NOT_MATCH.getLong(options)) + .execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public StorageObject get(StorageObject object, Map options) { + try { + return getRequest(object, options).execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + private Storage.Objects.Get getRequest(StorageObject object, Map options) + throws IOException { + return storage.objects() + .get(object.getBucket(), object.getName()) + .setProjection(DEFAULT_PROJECTION) + .setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(options)) + .setIfMetagenerationNotMatch(IF_METAGENERATION_NOT_MATCH.getLong(options)) + .setIfGenerationMatch(IF_GENERATION_MATCH.getLong(options)) + .setIfGenerationNotMatch(IF_GENERATION_NOT_MATCH.getLong(options)); + } + + @Override + public Bucket patch(Bucket bucket, Map options) { + try { + return storage.buckets() + .patch(bucket.getName(), bucket) + .setProjection(DEFAULT_PROJECTION) + .setPredefinedAcl(PREDEFINED_ACL.getString(options)) + .setPredefinedDefaultObjectAcl(PREDEFINED_DEFAULT_OBJECT_ACL.getString(options)) + .setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(options)) + .setIfMetagenerationNotMatch(IF_METAGENERATION_NOT_MATCH.getLong(options)) + .execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public StorageObject patch(StorageObject storageObject, Map options) { + try { + return patchRequest(storageObject, options).execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + private Storage.Objects.Patch patchRequest(StorageObject storageObject, Map options) + throws IOException { + return storage.objects() + .patch(storageObject.getBucket(), storageObject.getName(), storageObject) + .setProjection(DEFAULT_PROJECTION) + .setPredefinedAcl(PREDEFINED_ACL.getString(options)) + .setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(options)) + .setIfMetagenerationNotMatch(IF_METAGENERATION_NOT_MATCH.getLong(options)) + .setIfGenerationMatch(IF_GENERATION_MATCH.getLong(options)) + .setIfGenerationNotMatch(IF_GENERATION_NOT_MATCH.getLong(options)); + } + + @Override + public boolean delete(Bucket bucket, Map options) { + try { + storage.buckets() + .delete(bucket.getName()) + .setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(options)) + .setIfMetagenerationNotMatch(IF_METAGENERATION_NOT_MATCH.getLong(options)) + .execute(); + return true; + } catch (IOException ex) { + StorageException serviceException = translate(ex); + if (serviceException.code() == 404) { + return false; + } + throw serviceException; + } + } + + @Override + public boolean delete(StorageObject blob, Map options) { + try { + deleteRequest(blob, options).execute(); + return true; + } catch (IOException ex) { + StorageException serviceException = translate(ex); + if (serviceException.code() == 404) { + return false; + } + throw serviceException; + } + } + + private Storage.Objects.Delete deleteRequest(StorageObject blob, Map options) + throws IOException { + return storage.objects() + .delete(blob.getBucket(), blob.getName()) + .setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(options)) + .setIfMetagenerationNotMatch(IF_METAGENERATION_NOT_MATCH.getLong(options)) + .setIfGenerationMatch(IF_GENERATION_MATCH.getLong(options)) + .setIfGenerationMatch(100L) + .setIfGenerationNotMatch(IF_GENERATION_NOT_MATCH.getLong(options)); + } + + @Override + public StorageObject compose(Iterable sources, StorageObject target, + Map targetOptions) throws StorageException { + ComposeRequest request = new ComposeRequest(); + if (target.getContentType() == null) { + // todo: remove once this is no longer requirement (b/20681287). + target.setContentType("application/octet-stream"); + } + request.setDestination(target); + List sourceObjects = new ArrayList<>(); + for (StorageObject source : sources) { + ComposeRequest.SourceObjects sourceObject = new ComposeRequest.SourceObjects(); + sourceObject.setName(source.getName()); + Long generation = source.getGeneration(); + if (generation != null) { + sourceObject.setGeneration(generation); + sourceObject.setObjectPreconditions( + new ObjectPreconditions().setIfGenerationMatch(generation)); + } + sourceObjects.add(sourceObject); + } + request.setSourceObjects(sourceObjects); + try { + // todo: missing setProjection (b/20659000) + return storage.objects() + .compose(target.getBucket(), target.getName(), request) + .setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(targetOptions)) + .setIfGenerationMatch(IF_GENERATION_MATCH.getLong(targetOptions)) + .execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public StorageObject copy(StorageObject source, Map sourceOptions, + StorageObject target, Map targetOptions) throws StorageException { + try { + return storage + .objects() + .copy(source.getBucket(), source.getName(), target.getBucket(), target.getName(), + target.getContentType() != null ? target : null) + .setProjection(DEFAULT_PROJECTION) + .setIfMetagenerationMatch(IF_SOURCE_METAGENERATION_MATCH.getLong(sourceOptions)) + .setIfMetagenerationNotMatch(IF_SOURCE_METAGENERATION_NOT_MATCH.getLong(sourceOptions)) + .setIfGenerationMatch(IF_SOURCE_GENERATION_MATCH.getLong(sourceOptions)) + .setIfGenerationNotMatch(IF_SOURCE_GENERATION_NOT_MATCH.getLong(sourceOptions)) + .setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(targetOptions)) + .setIfMetagenerationNotMatch(IF_METAGENERATION_NOT_MATCH.getLong(targetOptions)) + .setIfGenerationMatch(IF_GENERATION_MATCH.getLong(targetOptions)) + .setIfGenerationNotMatch(IF_GENERATION_NOT_MATCH.getLong(targetOptions)) + .execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public byte[] load(StorageObject from, Map options) + throws StorageException { + try { + Storage.Objects.Get getRequest = storage.objects() + .get(from.getBucket(), from.getName()) + .setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(options)) + .setIfMetagenerationNotMatch(IF_METAGENERATION_NOT_MATCH.getLong(options)) + .setIfGenerationMatch(IF_GENERATION_MATCH.getLong(options)) + .setIfGenerationNotMatch(IF_GENERATION_NOT_MATCH.getLong(options)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + getRequest.getMediaHttpDownloader().setDirectDownloadEnabled(true); + getRequest.executeMediaAndDownloadTo(out); + return out.toByteArray(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public BatchResponse batch(BatchRequest request) throws StorageException { + com.google.api.client.googleapis.batch.BatchRequest batch = storage.batch(); + final Map> deletes = + Maps.newConcurrentMap(); + final Map> updates = + Maps.newConcurrentMap(); + final Map> gets = + Maps.newConcurrentMap(); + try { + for (final Tuple> tuple : request.toDelete) { + deleteRequest(tuple.x(), tuple.y()).queue(batch, new JsonBatchCallback() { + @Override + public void onSuccess(Void ignore, HttpHeaders responseHeaders) { + deletes.put(tuple.x(), Tuple.of(Boolean.TRUE, null)); + } + + @Override + public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) { + deletes.put(tuple.x(), Tuple.of(null, translate(e))); + } + }); + } + for (final Tuple> tuple : request.toUpdate) { + patchRequest(tuple.x(), tuple.y()).queue(batch, new JsonBatchCallback() { + @Override + public void onSuccess(StorageObject storageObject, HttpHeaders responseHeaders) { + updates.put(tuple.x(), + Tuple.of(storageObject, null)); + } + + @Override + public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) { + updates.put(tuple.x(), + Tuple.of(null, translate(e))); + } + }); + } + for (final Tuple> tuple : request.toGet) { + getRequest(tuple.x(), tuple.y()).queue(batch, new JsonBatchCallback() { + @Override + public void onSuccess(StorageObject storageObject, HttpHeaders responseHeaders) { + gets.put(tuple.x(), + Tuple.of(storageObject, null)); + } + + @Override + public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) { + gets.put(tuple.x(), + Tuple.of(null, translate(e))); + } + }); + } + batch.execute(); + } catch (IOException ex) { + throw translate(ex); + } + return new BatchResponse(deletes, updates, gets); + } + + @Override + public byte[] read(StorageObject from, Map options, long position, int bytes) + throws StorageException { + try { + Get req = storage.objects().get(from.getBucket(), from.getName()); + req.setIfMetagenerationMatch(IF_METAGENERATION_MATCH.getLong(options)) + .setIfMetagenerationNotMatch(IF_METAGENERATION_NOT_MATCH.getLong(options)) + .setIfGenerationMatch(IF_GENERATION_MATCH.getLong(options)) + .setIfGenerationNotMatch(IF_GENERATION_NOT_MATCH.getLong(options)); + MediaHttpDownloader downloader = req.getMediaHttpDownloader(); + // todo: Fix int casting (https://github.com/google/google-api-java-client/issues/937) + downloader.setContentRange(position, (int) position + bytes); + downloader.setDirectDownloadEnabled(true); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + req.executeMediaAndDownloadTo(output); + return output.toByteArray(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public void write(String uploadId, byte[] toWrite, int toWriteOffset, StorageObject dest, + long destOffset, int length, boolean last) throws StorageException { + try { + GenericUrl url = new GenericUrl(uploadId); + HttpRequest httpRequest = storage.getRequestFactory().buildPostRequest(url, + new ByteArrayContent(null, toWrite, toWriteOffset, length)); + long limit = destOffset + length; + StringBuilder range = new StringBuilder("bytes "); + range.append(destOffset).append('-').append(limit - 1).append('/'); + if (last) { + range.append(limit); + } else { + range.append('*'); + } + httpRequest.getHeaders().setContentRange(range.toString()); + int code; + String message; + IOException exception = null; + try { + HttpResponse response = httpRequest.execute(); + code = response.getStatusCode(); + message = response.getStatusMessage(); + } catch (HttpResponseException ex) { + exception = ex; + code = ex.getStatusCode(); + message = ex.getStatusMessage(); + } + if (!last && code != 308 || last && !(code == 200 || code == 201)) { + if (exception != null) { + throw exception; + } + GoogleJsonError error = new GoogleJsonError(); + error.setCode(code); + error.setMessage(message); + throw translate(error); + } + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public String open(StorageObject object, Map options) + throws StorageException { + try { + Insert req = storage.objects().insert(object.getBucket(), object); + GenericUrl url = req.buildHttpRequest().getUrl(); + String scheme = url.getScheme(); + String host = url.getHost(); + String path = "/upload" + url.getRawPath(); + url = new GenericUrl(scheme + "://" + host + path); + url.set("uploadType", "resumable"); + url.set("name", object.getName()); + for (Option option : options.keySet()) { + Object content = option.get(options); + if (content != null) { + url.set(option.value(), content.toString()); + } + } + JsonFactory jsonFactory = storage.getJsonFactory(); + HttpRequestFactory requestFactory = storage.getRequestFactory(); + HttpRequest httpRequest = + requestFactory.buildPostRequest(url, new JsonHttpContent(jsonFactory, object)); + httpRequest.getHeaders().set("X-Upload-Content-Type", + MoreObjects.firstNonNull(object.getContentType(), "application/octet-stream")); + HttpResponse response = httpRequest.execute(); + if (response.getStatusCode() != 200) { + GoogleJsonError error = new GoogleJsonError(); + error.setCode(response.getStatusCode()); + error.setMessage(response.getStatusMessage()); + throw translate(error); + } + return response.getHeaders().getLocation(); + } catch (IOException ex) { + throw translate(ex); + } + } +} + diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpc.java b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpc.java new file mode 100644 index 000000000000..bd5d9b782975 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpc.java @@ -0,0 +1 @@ +/* * Copyright 2015 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.gcloud.spi; import com.google.api.services.storage.model.Bucket; import com.google.api.services.storage.model.StorageObject; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.gcloud.storage.StorageException; import java.util.List; import java.util.Map; public interface StorageRpc { // These options are part of the Google Cloud storage header options enum Option { PREDEFINED_ACL("predefinedAcl"), PREDEFINED_DEFAULT_OBJECT_ACL("predefinedDefaultObjectAcl"), IF_METAGENERATION_MATCH("ifMetagenerationMatch"), IF_METAGENERATION_NOT_MATCH("ifMetagenerationNotMatch"), IF_GENERATION_NOT_MATCH("ifGenerationMatch"), IF_GENERATION_MATCH("ifGenerationNotMatch"), IF_SOURCE_METAGENERATION_MATCH("ifSourceMetagenerationMatch"), IF_SOURCE_METAGENERATION_NOT_MATCH("ifSourceMetagenerationNotMatch"), IF_SOURCE_GENERATION_MATCH("ifSourceGenerationMatch"), IF_SOURCE_GENERATION_NOT_MATCH("ifSourceGenerationNotMatch"), PREFIX("prefix"), MAX_RESULTS("maxResults"), PAGE_TOKEN("pageToken"), DELIMITER("delimiter"), VERSIONS("versions"); private final String value; Option(String value) { this.value = value; } public String value() { return value; } @SuppressWarnings("unchecked") T get(Map options) { return (T) options.get(this); } String getString(Map options) { return get(options); } Long getLong(Map options) { return get(options); } Boolean getBoolean(Map options) { return get(options); } } class Tuple { private final X x; private final Y y; private Tuple(X x, Y y) { this.x = x; this.y = y; } public static Tuple of(X x, Y y) { return new Tuple<>(x, y); } public X x() { return x; } public Y y() { return y; } } class BatchRequest { public final List>> toDelete; public final List>> toUpdate; public final List>> toGet; public BatchRequest(Iterable>> toDelete, Iterable>> toUpdate, Iterable>> toGet) { this.toDelete = ImmutableList.copyOf(toDelete); this.toUpdate = ImmutableList.copyOf(toUpdate); this.toGet = ImmutableList.copyOf(toGet); } } class BatchResponse { public final Map> deletes; public final Map> updates; public final Map> gets; public BatchResponse(Map> deletes, Map> updates, Map> gets) { this.deletes = ImmutableMap.copyOf(deletes); this.updates = ImmutableMap.copyOf(updates); this.gets = ImmutableMap.copyOf(gets); } } Bucket create(Bucket bucket, Map options) throws StorageException; StorageObject create(StorageObject object, byte[] content, Map options) throws StorageException; Tuple> list(Map options) throws StorageException; Tuple> list(String bucket, Map options) throws StorageException; Bucket get(Bucket bucket, Map options) throws StorageException; StorageObject get(StorageObject object, Map options) throws StorageException; Bucket patch(Bucket bucket, Map options) throws StorageException; StorageObject patch(StorageObject storageObject, Map options) throws StorageException; boolean delete(Bucket bucket, Map options) throws StorageException; boolean delete(StorageObject object, Map options) throws StorageException; BatchResponse batch(BatchRequest request) throws StorageException; StorageObject compose(Iterable sources, StorageObject target, Map targetOptions) throws StorageException; StorageObject copy(StorageObject source, Map sourceOptions, StorageObject target, Map targetOptions) throws StorageException; byte[] load(StorageObject storageObject, Map options) throws StorageException; byte[] read(StorageObject from, Map options, long position, int bytes) throws StorageException; String open(StorageObject object, Map options) throws StorageException; void write(String uploadId, byte[] toWrite, int toWriteOffset, StorageObject dest, long destOffset, int length, boolean last) throws StorageException; } \ No newline at end of file diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpcFactory.java b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpcFactory.java new file mode 100644 index 000000000000..f4959d617d17 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpcFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 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.gcloud.spi; + +import com.google.gcloud.storage.StorageOptions; + +/** + * An interface for Storage RPC factory. + * Implementation will be loaded via {@link java.util.ServiceLoader}. + */ +public interface StorageRpcFactory extends ServiceRpcFactory { +} + diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Acl.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Acl.java new file mode 100644 index 000000000000..d77bb1eaef02 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Acl.java @@ -0,0 +1,272 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import com.google.api.services.storage.model.BucketAccessControl; +import com.google.api.services.storage.model.ObjectAccessControl; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Access Control List on for buckets or blobs. + */ +public final class Acl implements Serializable { + + private static final long serialVersionUID = 6435575339887912222L; + + private final Entity entity; + private final Role role; + + public enum Role { + OWNER, READER, WRITER + } + + public static abstract class Entity implements Serializable { + + private static final long serialVersionUID = -2707407252771255840L; + + private final Type type; + private final String value; + + public enum Type { + DOMAIN, GROUP, USER, PROJECT, UNKNOWN + } + + Entity(Type type, String value) { + this.type = type; + this.value = value; + } + + public Type type() { + return type; + } + + protected String value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Entity entity = (Entity) o; + return Objects.equals(type, entity.type) && + Objects.equals(value, entity.value); + } + + @Override + public int hashCode() { + return Objects.hash(type, value); + } + + @Override + public String toString() { + return toPb(); + } + + String toPb() { + return type.name().toLowerCase() + "-" + value(); + } + + static Entity fromPb(String entity) { + if (entity.startsWith("user-")) { + return new User(entity.substring(5)); + } + if (entity.equals(User.ALL_USERS)) { + return User.ofAllUsers(); + } + if (entity.equals(User.ALL_AUTHENTICATED_USERS)) { + return User.ofAllAuthenticatedUsers(); + } + if (entity.startsWith("group-")) { + return new Group(entity.substring(6)); + } + if (entity.startsWith("domain-")) { + return new Domain(entity.substring(7)); + } + if (entity.startsWith("project-")) { + int idx = entity.indexOf('-', 8); + String team = entity.substring(8, idx); + String projectId = entity.substring(idx + 1); + return new Project(Project.ProjectRole.valueOf(team.toUpperCase()), projectId); + } + return new RawEntity(entity); + } + } + + public static final class Domain extends Entity { + + private static final long serialVersionUID = -3033025857280447253L; + + public Domain(String domain) { + super(Type.DOMAIN, domain); + } + + public String domain() { + return value(); + } + } + + public static final class Group extends Entity { + + private static final long serialVersionUID = -1660987136294408826L; + + public Group(String email) { + super(Type.GROUP, email); + } + + public String email() { + return value(); + } + } + + public static final class User extends Entity { + + private static final long serialVersionUID = 3076518036392737008L; + private static final String ALL_USERS = "allUsers"; + private static final String ALL_AUTHENTICATED_USERS = "allAuthenticatedUsers"; + + public User(String email) { + super(Type.USER, email); + } + + public String email() { + return value(); + } + + @Override + String toPb() { + switch (value()) { + case ALL_AUTHENTICATED_USERS: + return ALL_AUTHENTICATED_USERS; + case ALL_USERS: + return ALL_USERS; + default: + break; + } + return super.toPb(); + } + + public static User ofAllUsers() { + return new User(ALL_USERS); + } + + public static User ofAllAuthenticatedUsers() { + return new User(ALL_AUTHENTICATED_USERS); + } + } + + public static final class Project extends Entity { + + private static final long serialVersionUID = 7933776866530023027L; + + private final ProjectRole pRole; + private final String projectId; + + enum ProjectRole { + OWNERS, EDITORS, VIEWERS + } + + public Project(ProjectRole pRole, String projectId) { + super(Type.PROJECT, pRole.name().toLowerCase() + "-" + projectId); + this.pRole = pRole; + this.projectId = projectId; + } + + public ProjectRole projectRole() { + return pRole; + } + + public String projectId() { + return projectId; + } + } + + public static final class RawEntity extends Entity { + + private static final long serialVersionUID = 3966205614223053950L; + + RawEntity(String entity) { + super(Type.UNKNOWN, entity); + } + + @Override + String toPb() { + return value(); + } + } + + public Acl(Entity entity, Role role) { + this.entity = entity; + this.role = role; + } + + public Entity entity() { + return entity; + } + + public Role role() { + return role; + } + + @Override + public int hashCode() { + return Objects.hash(entity, role); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Acl other = (Acl) obj; + return Objects.equals(this.entity, other.entity) + && Objects.equals(this.role, other.role); + } + + BucketAccessControl toBucketPb() { + BucketAccessControl bucketPb = new BucketAccessControl(); + bucketPb.setRole(role().toString()); + bucketPb.setEntity(entity().toString()); + return bucketPb; + } + + ObjectAccessControl toObjectPb() { + ObjectAccessControl objectPb = new ObjectAccessControl(); + objectPb.setRole(role().name()); + objectPb.setEntity(entity().toPb()); + return objectPb; + } + + static Acl fromPb(ObjectAccessControl objectAccessControl) { + Role role = Role.valueOf(objectAccessControl.getRole()); + return new Acl(Entity.fromPb(objectAccessControl.getEntity()), role); + } + + static Acl fromPb(BucketAccessControl bucketAccessControl) { + Role role = Role.valueOf(bucketAccessControl.getRole()); + return new Acl(Entity.fromPb(bucketAccessControl.getEntity()), role); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchRequest.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchRequest.java new file mode 100644 index 000000000000..6959388f34ef --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchRequest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.gcloud.storage.Storage.BlobSourceOption; +import com.google.gcloud.storage.Storage.BlobTargetOption; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Google storage batch request. + */ +public final class BatchRequest implements Serializable { + + private static final long serialVersionUID = -1527992265939800345L; + + private final Map> toDelete; + private final Map> toUpdate; + private final Map> toGet; + + public static class Builder { + + private Map> toDelete = new LinkedHashMap<>(); + private Map> toUpdate = new LinkedHashMap<>(); + private Map> toGet = new LinkedHashMap<>(); + + private Builder() {} + + /** + * Delete the given blob. + */ + public Builder delete(String bucket, String blob, BlobSourceOption... options) { + toDelete.put(BlobInfo.of(bucket, blob), Lists.newArrayList(options)); + return this; + } + + /** + * Update the given blob. + */ + public Builder update(BlobInfo blobInfo, BlobTargetOption... options) { + toUpdate.put(blobInfo, Lists.newArrayList(options)); + return this; + } + + /** + * Retrieve metadata for the given blob. + */ + public Builder get(String bucket, String blob, BlobSourceOption... options) { + toGet.put(BlobInfo.of(bucket, blob), Lists.newArrayList(options)); + return this; + } + + public BatchRequest build() { + return new BatchRequest(this); + } + } + + private BatchRequest(Builder builder) { + toDelete = ImmutableMap.copyOf(builder.toDelete); + toUpdate = ImmutableMap.copyOf(builder.toUpdate); + toGet = ImmutableMap.copyOf(builder.toGet); + } + + @Override + public int hashCode() { + return Objects.hash(toDelete, toUpdate, toGet); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof BatchRequest)) { + return false; + } + BatchRequest other = (BatchRequest) obj; + return Objects.equals(toDelete, other.toDelete) + && Objects.equals(toUpdate, other.toUpdate) + && Objects.equals(toGet, other.toGet); + } + + public Map> toDelete() { + return toDelete; + } + + public Map> toUpdate() { + return toUpdate; + } + + public Map> toGet() { + return toGet; + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchResponse.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchResponse.java new file mode 100644 index 000000000000..45aa1674b03c --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BatchResponse.java @@ -0,0 +1,158 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +/** + * Google Storage batch response. + */ +public final class BatchResponse implements Serializable { + + private static final long serialVersionUID = 1057416839397037706L; + + private final List> deleteResult; + private final List> updateResult; + private final List> getResult; + + public static class Result implements Serializable { + + private static final long serialVersionUID = -1946539570170529094L; + private static final Result EMPTY = new BatchResponse.Result(null); + + private final T value; + private final StorageException exception; + + + public Result(T value) { + this.value = value; + this.exception = null; + } + + public Result(StorageException exception) { + this.exception = exception; + this.value = null; + } + + static Result of(T value) { + return new Result<>(value); + } + + /** + * Returns the result. + * + * @throws StorageException if failed + */ + public T get() throws StorageException { + if (failed()) { + throw failure(); + } + return value; + } + + /** + * Returns the failure or {@code null} if was successful. + */ + public StorageException failure() { + return exception; + } + + /** + * Returns {@code true} if failed, {@code false} otherwise. + */ + public boolean failed() { + return exception != null; + } + + @Override + public int hashCode() { + return Objects.hash(value, exception); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Result)) { + return false; + } + Result other = (Result) obj; + return Objects.equals(value, other.value) + && Objects.equals(exception, other.exception); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("value", value) + .add("exception", exception) + .toString(); + } + + @SuppressWarnings("unchecked") + static Result empty() { + return EMPTY; + } + } + + public BatchResponse(List> deleteResult, List> updateResult, + List> getResult) { + this.deleteResult = ImmutableList.copyOf(deleteResult); + this.updateResult = ImmutableList.copyOf(updateResult); + this.getResult = ImmutableList.copyOf(getResult); + } + + @Override + public int hashCode() { + return Objects.hash(deleteResult, updateResult, getResult); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof BatchResponse)) { + return false; + } + BatchResponse other = (BatchResponse) obj; + return Objects.equals(deleteResult, other.deleteResult) + && Objects.equals(updateResult, other.updateResult) + && Objects.equals(updateResult, other.updateResult); + } + + /** + * Returns the results for the delete operations using the request order. + */ + public List> deletes() { + return deleteResult; + } + + /** + * Returns the results for the update operations using the request order. + */ + public List> updates() { + return updateResult; + } + + /** + * Returns the results for the get operations using the request order. + */ + public List> gets() { + return getResult; + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobInfo.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobInfo.java new file mode 100644 index 000000000000..3ef01af8d1f3 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobInfo.java @@ -0,0 +1,491 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.util.Data; +import com.google.api.client.util.DateTime; +import com.google.api.services.storage.model.ObjectAccessControl; +import com.google.api.services.storage.model.StorageObject; +import com.google.api.services.storage.model.StorageObject.Owner; +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.common.collect.Lists; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * A Google Storage object. + * + * @see Concepts and Terminology + */ +public final class BlobInfo implements Serializable { + + private static final long serialVersionUID = 2228487739943277159L; + + static final Function FROM_PB_FUNCTION = + new Function() { + @Override + public BlobInfo apply(StorageObject pb) { + return BlobInfo.fromPb(pb); + } + }; + + static final Function TO_PB_FUNCTION = + new Function() { + @Override + public StorageObject apply(BlobInfo blobInfo) { + return blobInfo.toPb(); + } + }; + + private final String bucket; + private final String id; + private final String name; + private final String selfLink; + private final String cacheControl; + private final List acl; + private final Acl.Entity owner; + private final Long size; + private final String etag; + private final String md5; + private final String crc32c; + private final String mediaLink; + private final Map metadata; + private final Long generation; + private final Long metageneration; + private final Long deleteTime; + private final Long updateTime; + private final String contentType; + private final String contentEncoding; + private final String contentDisposition; + private final String contentLanguage; + private final Integer componentCount; + + public static final class Builder { + + private String bucket; + private String id; + private String name; + private String contentType; + private String contentEncoding; + private String contentDisposition; + private String contentLanguage; + private Integer componentCount; + private String cacheControl; + private ImmutableList acl; + private Acl.Entity owner; + private Long size; + private String etag; + private String selfLink; + private String md5; + private String crc32c; + private String mediaLink; + private ImmutableMap metadata; + private Long generation; + private Long metageneration; + private Long deleteTime; + private Long updateTime; + + private Builder() {} + + public Builder bucket(String bucket) { + this.bucket = checkNotNull(bucket); + return this; + } + + Builder id(String id) { + this.id = id; + return this; + } + + public Builder name(String name) { + this.name = checkNotNull(name); + return this; + } + + public Builder contentType(String contentType) { + this.contentType = firstNonNull(contentType, Data.nullOf(String.class)); + return this; + } + + public Builder contentDisposition(String contentDisposition) { + this.contentDisposition = firstNonNull(contentDisposition, Data.nullOf(String.class)); + return this; + } + + public Builder contentLanguage(String contentLanguage) { + this.contentLanguage = firstNonNull(contentLanguage, Data.nullOf(String.class)); + return this; + } + + public Builder contentEncoding(String contentEncoding) { + this.contentEncoding = firstNonNull(contentEncoding, Data.nullOf(String.class)); + return this; + } + + Builder componentCount(Integer componentCount) { + this.componentCount = componentCount; + return this; + } + + public Builder cacheControl(String cacheControl) { + this.cacheControl = firstNonNull(cacheControl, Data.nullOf(String.class)); + return this; + } + + public Builder acl(List acl) { + this.acl = acl != null ? ImmutableList.copyOf(acl) : null; + return this; + } + + Builder owner(Acl.Entity owner) { + this.owner = owner; + return this; + } + + Builder size(Long size) { + this.size = size; + return this; + } + + Builder etag(String etag) { + this.etag = etag; + return this; + } + + Builder selfLink(String selfLink) { + this.selfLink = selfLink; + return this; + } + + public Builder md5(String md5) { + this.md5 = firstNonNull(md5, Data.nullOf(String.class)); + return this; + } + + public Builder crc32c(String crc32c) { + this.crc32c = firstNonNull(crc32c, Data.nullOf(String.class)); + return this; + } + + Builder mediaLink(String mediaLink) { + this.mediaLink = mediaLink; + return this; + } + + public Builder metadata(Map metadata) { + this.metadata = metadata != null ? ImmutableMap.copyOf(metadata) : null; + return this; + } + + Builder generation(Long generation) { + this.generation = generation; + return this; + } + + Builder metageneration(Long metageneration) { + this.metageneration = metageneration; + return this; + } + + Builder deleteTime(Long deleteTime) { + this.deleteTime = deleteTime; + return this; + } + + Builder updateTime(Long updateTime) { + this.updateTime = updateTime; + return this; + } + + public BlobInfo build() { + checkNotNull(bucket); + checkNotNull(name); + return new BlobInfo(this); + } + } + + private BlobInfo(Builder builder) { + bucket = builder.bucket; + name = builder.name; + id = builder.id; + cacheControl = builder.cacheControl; + contentEncoding = builder.contentEncoding; + contentType = builder.contentType; + contentDisposition = builder.contentDisposition; + contentLanguage = builder.contentLanguage; + componentCount = builder.componentCount; + acl = builder.acl; + owner = builder.owner; + size = builder.size; + etag = builder.etag; + selfLink = builder.selfLink; + md5 = builder.md5; + crc32c = builder.crc32c; + mediaLink = builder.mediaLink; + metadata = builder.metadata; + generation = builder.generation; + metageneration = builder.metageneration; + deleteTime = builder.deleteTime; + updateTime = builder.updateTime; + } + + public String bucket() { + return bucket; + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public String cacheControl() { + return Data.isNull(cacheControl) ? null : cacheControl; + } + + public List acl() { + return acl; + } + + public Acl.Entity owner() { + return owner; + } + + public Long size() { + return size; + } + + public String contentType() { + return Data.isNull(contentType) ? null : contentType; + } + + public String contentEncoding() { + return Data.isNull(contentEncoding) ? null : contentEncoding; + } + + public String contentDisposition() { + return Data.isNull(contentDisposition) ? null : contentDisposition; + } + + public String contentLanguage() { + return Data.isNull(contentLanguage) ? null : contentLanguage; + } + + public Integer componentCount() { + return componentCount; + } + + public String etag() { + return etag; + } + + public String selfLink() { + return selfLink; + } + + public String md5() { + return Data.isNull(md5) ? null : md5; + } + + public String crc32c() { + return Data.isNull(crc32c) ? null : crc32c; + } + + public String mediaLink() { + return mediaLink; + } + + public Map metadata() { + return metadata; + } + + public Long generation() { + return generation; + } + + public Long metageneration() { + return metageneration; + } + + public Long deleteTime() { + return deleteTime; + } + + public Long updateTime() { + return updateTime; + } + + public Builder toBuilder() { + return new Builder() + .bucket(bucket) + .name(name) + .id(id) + .generation(generation) + .cacheControl(cacheControl) + .contentEncoding(contentEncoding) + .contentType(contentType) + .contentDisposition(contentDisposition) + .contentLanguage(contentLanguage) + .componentCount(componentCount) + .crc32c(crc32c) + .md5(md5) + .deleteTime(deleteTime) + .updateTime(updateTime) + .mediaLink(mediaLink) + .metadata(metadata) + .metageneration(metageneration) + .acl(acl) + .owner(owner) + .size(size) + .etag(etag) + .selfLink(selfLink); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("bucket", bucket()) + .add("name", name()) + .add("size", size()) + .add("content-type", contentType()) + .add("metadata", metadata()) + .toString(); + } + + public static BlobInfo of(String bucket, String name) { + return builder(bucket, name).build(); + } + + public static Builder builder(BucketInfo bucketInfo, String name) { + return builder(bucketInfo.name(), name); + } + + public static Builder builder(String bucket, String name) { + return new Builder().bucket(bucket).name(name); + } + + @Override + public int hashCode() { + return Objects.hash(bucket, name); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof BlobInfo)) { + return false; + } + return Objects.equals(toPb(), ((BlobInfo) obj).toPb()); + } + + StorageObject toPb() { + StorageObject storageObject = new StorageObject(); + if (acl != null) { + storageObject.setAcl(Lists.transform(acl, new Function() { + @Override + public ObjectAccessControl apply(Acl acl) { + return acl.toObjectPb(); + } + })); + } + if (deleteTime != null) { + storageObject.setTimeDeleted(new DateTime(deleteTime)); + } + if (updateTime != null) { + storageObject.setUpdated(new DateTime(updateTime)); + } + if (size != null) { + storageObject.setSize(BigInteger.valueOf(size)); + } + if (owner != null) { + storageObject.setOwner(new Owner().setEntity(owner.toPb())); + } + storageObject.setBucket(bucket); + storageObject.setCacheControl(cacheControl); + storageObject.setContentEncoding(contentEncoding); + storageObject.setCrc32c(crc32c); + storageObject.setContentType(contentType); + storageObject.setGeneration(generation); + storageObject.setMd5Hash(md5); + storageObject.setMediaLink(mediaLink); + storageObject.setMetadata(metadata); + storageObject.setMetageneration(metageneration); + storageObject.setName(name); + storageObject.setContentDisposition(contentDisposition); + storageObject.setComponentCount(componentCount); + storageObject.setContentLanguage(contentLanguage); + storageObject.setEtag(etag); + storageObject.setId(id); + storageObject.setSelfLink(selfLink); + return storageObject; + } + + static BlobInfo fromPb(StorageObject storageObject) { + Builder builder = new Builder() + .bucket(storageObject.getBucket()) + .cacheControl(storageObject.getCacheControl()) + .contentEncoding(storageObject.getContentEncoding()) + .crc32c(storageObject.getCrc32c()) + .contentType(storageObject.getContentType()) + .generation(storageObject.getGeneration()) + .md5(storageObject.getMd5Hash()) + .mediaLink(storageObject.getMediaLink()) + .metageneration(storageObject.getMetageneration()) + .name(storageObject.getName()) + .contentDisposition(storageObject.getContentDisposition()) + .componentCount(storageObject.getComponentCount()) + .contentLanguage(storageObject.getContentLanguage()) + .etag(storageObject.getEtag()) + .id(storageObject.getId()) + .selfLink(storageObject.getSelfLink()); + if (storageObject.getMetadata() != null) { + builder.metadata(storageObject.getMetadata()); + } + if (storageObject.getTimeDeleted() != null) { + builder.deleteTime(storageObject.getTimeDeleted().getValue()); + } + if (storageObject.getUpdated() != null) { + builder.updateTime(storageObject.getUpdated().getValue()); + } + if (storageObject.getSize() != null) { + builder.size(storageObject.getSize().longValue()); + } + if (storageObject.getOwner() != null) { + builder.owner(Acl.Entity.fromPb(storageObject.getOwner().getEntity())); + } + if (storageObject.getAcl() != null) { + builder.acl(Lists.transform(storageObject.getAcl(), new Function() { + @Override + public Acl apply(ObjectAccessControl objectAccessControl) { + return Acl.fromPb(objectAccessControl); + } + })); + } + return builder.build(); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobReadChannel.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobReadChannel.java new file mode 100644 index 000000000000..ad1a385d9a83 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobReadChannel.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Serializable; +import java.nio.channels.ReadableByteChannel; + +/** + * A channel for reading data from a Google Cloud Storage object. + * + * Implementations of this class may buffer data internally to reduce remote calls. + * + * This class is @{link Serializable}, which allows incremental reads. + */ +public interface BlobReadChannel extends ReadableByteChannel, Serializable, Closeable { + + /** + * Overridden to remove IOException. + * + * @see java.nio.channels.Channel#close() + */ + @Override + void close(); + + void seek(int position) throws IOException; + + /** + * Sets the minimum size that will be read by a single RPC. + * Read data will be locally buffered until consumed. + */ + void chunkSize(int chunkSize); + +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobReadChannelImpl.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobReadChannelImpl.java new file mode 100644 index 000000000000..9d1d37f93ab1 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobReadChannelImpl.java @@ -0,0 +1,146 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static com.google.gcloud.RetryHelper.runWithRetries; + +import com.google.api.services.storage.model.StorageObject; +import com.google.gcloud.spi.StorageRpc; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.Callable; + +/** + * Default implementation for BlobReadChannel. + */ +class BlobReadChannelImpl implements BlobReadChannel { + + private static final int DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024; + private static final long serialVersionUID = 4821762590742862669L; + + private final StorageOptions serviceOptions; + private final BlobInfo blobInfo; + private final Map requestOptions; + private int position; + private boolean isOpen; + private boolean endOfStream; + private int chunkSize = DEFAULT_CHUNK_SIZE; + + private transient StorageRpc storageRpc; + private transient StorageObject storageObject; + private transient int bufferPos; + private transient byte[] buffer; + + BlobReadChannelImpl(StorageOptions serviceOptions, BlobInfo blobInfo, + Map requestOptions) { + this.serviceOptions = serviceOptions; + this.blobInfo = blobInfo; + this.requestOptions = requestOptions; + isOpen = true; + initTransients(); + } + + private void writeObject(ObjectOutputStream out) throws IOException { + if (buffer != null) { + position += bufferPos; + buffer = null; + bufferPos = 0; + endOfStream = false; + } + out.defaultWriteObject(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + initTransients(); + } + + private void initTransients() { + storageRpc = serviceOptions.storageRpc(); + storageObject = blobInfo.toPb(); + } + + @Override + public boolean isOpen() { + return isOpen; + } + + @Override + public void close() { + if (isOpen) { + buffer = null; + isOpen = false; + } + } + + private void validateOpen() throws IOException { + if (!isOpen) { + throw new IOException("stream is closed"); + } + } + + @Override + public void seek(int position) throws IOException { + validateOpen(); + this.position = position; + buffer = null; + bufferPos = 0; + endOfStream = false; + } + + @Override + public void chunkSize(int chunkSize) { + this.chunkSize = chunkSize <= 0 ? DEFAULT_CHUNK_SIZE : chunkSize; + } + + @Override + public int read(ByteBuffer byteBuffer) throws IOException { + validateOpen(); + if (buffer == null) { + if (endOfStream) { + return -1; + } + final int toRead = Math.max(byteBuffer.remaining(), chunkSize); + buffer = runWithRetries(new Callable() { + @Override + public byte[] call() { + return storageRpc.read(storageObject, requestOptions, position, toRead); + } + }, serviceOptions.retryParams(), StorageImpl.EXCEPTION_HANDLER); + if (toRead > buffer.length) { + endOfStream = true; + if (buffer.length == 0) { + buffer = null; + return -1; + } + } + } + int toWrite = Math.min(buffer.length - bufferPos, byteBuffer.remaining()); + byteBuffer.put(buffer, bufferPos, toWrite); + bufferPos += toWrite; + if (bufferPos >= buffer.length) { + position += buffer.length; + buffer = null; + bufferPos = 0; + } + return toWrite; + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobWriteChannel.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobWriteChannel.java new file mode 100644 index 000000000000..20b2ce087632 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobWriteChannel.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import java.io.Closeable; +import java.io.Serializable; +import java.nio.channels.WritableByteChannel; + +/** + * A channel for writing data to a Google Cloud Storage object. + * + * Implementations of this class may further buffer data internally to reduce remote calls. Written + * data will only be visible after calling {@link #close()}. This class is serializable, to allow + * incremental writes. + */ +public interface BlobWriteChannel extends WritableByteChannel, Serializable, Closeable { + + /** + * Sets the minimum size that will be written by a single RPC. + * Written data will be buffered and only flushed upon reaching this size or closing the channel. + */ + void chunkSize(int chunkSize); +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobWriterChannelImpl.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobWriterChannelImpl.java new file mode 100644 index 000000000000..27cd807f043f --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BlobWriterChannelImpl.java @@ -0,0 +1,144 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static com.google.gcloud.RetryHelper.runWithRetries; +import static java.util.concurrent.Executors.callable; + +import com.google.api.services.storage.model.StorageObject; +import com.google.gcloud.spi.StorageRpc; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Map; + +/** + * Default implementation for BlobWriteChannel. + */ +class BlobWriterChannelImpl implements BlobWriteChannel { + + private static final long serialVersionUID = 8675286882724938737L; + private static final int MIN_CHUNK_SIZE = 256 * 1024; + private static final int DEFAULT_CHUNK_SIZE = 8 * MIN_CHUNK_SIZE; + + private final StorageOptions options; + private final BlobInfo blobInfo; + private final String uploadId; + private int position; + private byte[] buffer = new byte[0]; + private int limit; + private boolean isOpen = true; + private int chunkSize = DEFAULT_CHUNK_SIZE; + + private transient StorageRpc storageRpc; + private transient StorageObject storageObject; + + public BlobWriterChannelImpl(StorageOptions options, BlobInfo blobInfo, + Map optionsMap) { + this.options = options; + this.blobInfo = blobInfo; + initTransients(); + uploadId = options.storageRpc().open(storageObject, optionsMap); + } + + private void writeObject(ObjectOutputStream out) throws IOException { + if (isOpen) { + flush(true); + } + out.defaultWriteObject(); + } + + private void flush(boolean compact) { + if (limit >= chunkSize || compact && limit >= MIN_CHUNK_SIZE) { + final int length = limit - limit % MIN_CHUNK_SIZE; + runWithRetries(callable(new Runnable() { + @Override + public void run() { + storageRpc.write(uploadId, buffer, 0, storageObject, position, length, false); + } + }), options.retryParams(), StorageImpl.EXCEPTION_HANDLER); + position += length; + limit -= length; + byte[] temp = new byte[compact ? limit : chunkSize]; + System.arraycopy(buffer, length, temp, 0, limit); + buffer = temp; + } + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + if (isOpen) { + initTransients(); + } + } + + private void initTransients() { + storageRpc = options.storageRpc(); + storageObject = blobInfo.toPb(); + } + + private void validateOpen() throws IOException { + if (!isOpen) { + throw new IOException("stream is closed"); + } + } + + @Override + public int write(ByteBuffer byteBuffer) throws IOException { + validateOpen(); + int toWrite = byteBuffer.remaining(); + int spaceInBuffer = buffer.length - limit; + if (spaceInBuffer >= toWrite) { + byteBuffer.get(buffer, limit, toWrite); + } else { + buffer = Arrays.copyOf(buffer, Math.max(chunkSize, buffer.length + toWrite - spaceInBuffer)); + byteBuffer.get(buffer, limit, toWrite); + } + limit += toWrite; + flush(false); + return toWrite; + } + + @Override + public boolean isOpen() { + return isOpen; + } + + @Override + public void close() throws IOException { + if (isOpen) { + runWithRetries(callable(new Runnable() { + @Override + public void run() { + storageRpc.write(uploadId, buffer, 0, storageObject, position, limit, true); + } + }), options.retryParams(), StorageImpl.EXCEPTION_HANDLER); + position += buffer.length; + isOpen = false; + buffer = null; + } + } + + @Override + public void chunkSize(int chunkSize) { + chunkSize = (chunkSize / MIN_CHUNK_SIZE) * MIN_CHUNK_SIZE; + this.chunkSize = Math.max(MIN_CHUNK_SIZE, chunkSize); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BucketInfo.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BucketInfo.java new file mode 100644 index 000000000000..60926e01dbb2 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/BucketInfo.java @@ -0,0 +1,748 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static com.google.api.client.repackaged.com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.collect.Lists.transform; + +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.Data; +import com.google.api.client.util.DateTime; +import com.google.api.services.storage.model.Bucket.Lifecycle; +import com.google.api.services.storage.model.Bucket.Lifecycle.Rule; +import com.google.api.services.storage.model.Bucket.Owner; +import com.google.api.services.storage.model.Bucket.Versioning; +import com.google.api.services.storage.model.Bucket.Website; +import com.google.api.services.storage.model.BucketAccessControl; +import com.google.api.services.storage.model.ObjectAccessControl; +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.gcloud.storage.Acl.Entity; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +/** + * A Google Storage bucket. + * + * @see Concepts and Terminology + */ +public final class BucketInfo implements Serializable { + + private static final long serialVersionUID = -3946094202176916586L; + + private final String id; + private final String name; + private final Acl.Entity owner; + private final String selfLink; + private final Boolean versioningEnabled; + private final String indexPage; + private final String notFoundPage; + private final List deleteRules; + private final String etag; + private final Long createTime; + private final Long metageneration; + private final List cors; + private final List acl; + private final List defaultAcl; + private final Location location; + private final StorageClass storageClass; + + static final Function FROM_PB_FUNCTION = + new Function() { + @Override + public BucketInfo apply(com.google.api.services.storage.model.Bucket pb) { + return BucketInfo.fromPb(pb); + } + }; + + static final Function TO_PB_FUNCTION = + new Function() { + @Override + public com.google.api.services.storage.model.Bucket apply(BucketInfo bucketInfo) { + return bucketInfo.toPb(); + } + }; + + public static abstract class DeleteRule implements Serializable { + + private static final long serialVersionUID = 3137971668395933033L; + private final Type type; + private static final String SUPPORTED_ACTION = "Delete"; + + public enum Type { + AGE, CREATE_BEFORE, NUM_NEWER_VERSIONS, IS_LIVE, UNKNOWN + } + + DeleteRule(Type type) { + this.type = type; + } + + public Type type() { + return type; + } + + @Override + public int hashCode() { + return Objects.hash(type); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final DeleteRule other = (DeleteRule) obj; + return Objects.equals(toPb(), other.toPb()); + } + + Rule toPb() { + Rule rule = new Rule(); + rule.setAction(new Rule.Action().setType(SUPPORTED_ACTION)); + Rule.Condition condition = new Rule.Condition(); + populateCondition(condition); + rule.setCondition(condition); + return rule; + } + + abstract void populateCondition(Rule.Condition condition); + + static DeleteRule fromPb(Rule rule) { + if (rule.getAction() != null && SUPPORTED_ACTION.endsWith(rule.getAction().getType())) { + Rule.Condition condition = rule.getCondition(); + Integer age = condition.getAge(); + if (age != null) { + return new AgeDeleteRule(age); + } + DateTime dateTime = condition.getCreatedBefore(); + if (dateTime != null) { + return new CreatedBeforeDeleteRule(dateTime.getValue()); + } + Integer numNewerVersions = condition.getNumNewerVersions(); + if (numNewerVersions != null) { + return new NumNewerVersionsDeleteRule(numNewerVersions); + } + Boolean isLive = condition.getIsLive(); + if (isLive != null) { + return new IsLiveDeleteRule(isLive); + } + } + return new RawDeleteRule(rule); + } + } + + public static class AgeDeleteRule extends DeleteRule { + + private static final long serialVersionUID = 5697166940712116380L; + private final int daysToLive; + + public AgeDeleteRule(int daysToLive) { + super(Type.AGE); + this.daysToLive = daysToLive; + } + + public int daysToLive() { + return daysToLive; + } + + void populateCondition(Rule.Condition condition) { + condition.setAge(daysToLive); + } + } + + static class RawDeleteRule extends DeleteRule { + + private static final long serialVersionUID = -7166938278642301933L; + + private transient Rule rule; + + RawDeleteRule(Rule rule) { + super(Type.UNKNOWN); + this.rule = rule; + } + + void populateCondition(Rule.Condition condition) { + throw new UnsupportedOperationException(); + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + out.writeUTF(rule.toString()); + } + + private void readObject(ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + rule = new JacksonFactory().fromString(in.readUTF(), Rule.class); + } + + Rule toPb() { + return rule; + } + } + + public static class CreatedBeforeDeleteRule extends DeleteRule { + + private static final long serialVersionUID = 881692650279195867L; + private final long timeMillis; + + public CreatedBeforeDeleteRule(long timeMillis) { + super(Type.CREATE_BEFORE); + this.timeMillis = timeMillis; + } + + public long timeMillis() { + return timeMillis; + } + + void populateCondition(Rule.Condition condition) { + condition.setCreatedBefore(new DateTime(timeMillis)); + } + } + + public static class NumNewerVersionsDeleteRule extends DeleteRule { + + private static final long serialVersionUID = -1955554976528303894L; + private final int numNewerVersions; + + public NumNewerVersionsDeleteRule(int numNewerVersions) { + super(Type.NUM_NEWER_VERSIONS); + this.numNewerVersions = numNewerVersions; + } + + public int numNewerVersions() { + return numNewerVersions; + } + + void populateCondition(Rule.Condition condition) { + condition.setNumNewerVersions(numNewerVersions); + } + } + + public static class IsLiveDeleteRule extends DeleteRule { + + private static final long serialVersionUID = -3502994563121313364L; + private final boolean isLive; + + public IsLiveDeleteRule(boolean isLive) { + super(Type.IS_LIVE); + this.isLive = isLive; + } + + public boolean isLive() { + return isLive; + } + + void populateCondition(Rule.Condition condition) { + condition.setIsLive(isLive); + } + } + + public static final class StorageClass implements Serializable { + + private static final long serialVersionUID = 374002156285326563L; + private static final ImmutableMap STRING_TO_OPTION; + private static final StorageClass NULL_VALUE = + new StorageClass(Data.nullOf(String.class)); + + private final String value; + + public enum Option { + DURABLE_REDUCED_AVAILABILITY, STANDARD; + + private final StorageClass storageClass; + + Option() { + storageClass = new StorageClass(name()); + } + } + + static { + ImmutableMap.Builder map = ImmutableMap.builder(); + for (Option option : Option.values()) { + map.put(option.name(), option); + } + STRING_TO_OPTION = map.build(); + } + + private StorageClass(String value) { + this.value = checkNotNull(value); + } + + public static StorageClass standard() { + return Option.STANDARD.storageClass; + } + + public static StorageClass durableReducedAvailability() { + return Option.DURABLE_REDUCED_AVAILABILITY.storageClass; + } + + public static StorageClass of(String value) { + Option option = STRING_TO_OPTION.get(value.toUpperCase()); + return option == null ? new StorageClass(value) : option.storageClass; + } + + @Override + public String toString() { + return value(); + } + + public String value() { + return value; + } + } + + public static final class Location implements Serializable { + + private static final long serialVersionUID = 9073107666838637662L; + private static final ImmutableMap STRING_TO_OPTION; + private static final Location NULL_VALUE = new Location(Data.nullOf(String.class)); + + private final String value; + + public enum Option { + US, EU, ASIA; + + private final Location location; + + Option() { + location = new Location(name()); + } + } + + static { + ImmutableMap.Builder map = ImmutableMap.builder(); + for (Option option : Option.values()) { + map.put(option.name(), option); + } + STRING_TO_OPTION = map.build(); + } + + private Location(String value) { + this.value = checkNotNull(value); + } + + public static Location us() { + return Option.US.location; + } + + public static Location eu() { + return Option.EU.location; + } + + public static Location asia() { + return Option.ASIA.location; + } + + public static Location of(String value) { + Option option = STRING_TO_OPTION.get(value.toUpperCase()); + return option == null ? new Location(value) : option.location; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Location other = (Location) obj; + return Objects.equals(this.value, other.value); + } + + @Override + public String toString() { + return value(); + } + + public String value() { + return value; + } + } + + public final static class Builder { + + private String id; + private String name; + private Acl.Entity owner; + private String selfLink; + private Boolean versioningEnabled; + private String indexPage; + private String notFoundPage; + private ImmutableList deleteRules; + private StorageClass storageClass; + private Location location; + private String etag; + private Long createTime; + private Long metageneration; + private ImmutableList cors; + private ImmutableList acl; + private ImmutableList defaultAcl; + + private Builder() {} + + public Builder name(String name) { + this.name = checkNotNull(name); + return this; + } + + Builder id(String id) { + this.id = id; + return this; + } + + Builder owner(Acl.Entity owner) { + this.owner = owner; + return this; + } + + Builder selfLink(String selfLink) { + this.selfLink = selfLink; + return this; + } + + public Builder versioningEnabled(Boolean enable) { + this.versioningEnabled = firstNonNull(enable, Data.nullOf(Boolean.class)); + return this; + } + + public Builder indexPage(String indexPage) { + this.indexPage = indexPage; + return this; + } + + public Builder notFoundPage(String notFoundPage) { + this.notFoundPage = notFoundPage; + return this; + } + + public Builder deleteRules(Iterable rules) { + this.deleteRules = ImmutableList.copyOf(rules); + return this; + } + + public Builder storageClass(StorageClass storageClass) { + this.storageClass = firstNonNull(storageClass, StorageClass.NULL_VALUE); + return this; + } + + public Builder location(Location location) { + this.location = firstNonNull(location, Location.NULL_VALUE); + return this; + } + + Builder etag(String etag) { + this.etag = etag; + return this; + } + + Builder createTime(Long createTime) { + this.createTime = createTime; + return this; + } + + Builder metageneration(Long metageneration) { + this.metageneration = metageneration; + return this; + } + + public Builder cors(Iterable cors) { + this.cors = cors != null ? ImmutableList.copyOf(cors) : null; + return this; + } + + public Builder acl(Iterable acl) { + this.acl = acl != null ? ImmutableList.copyOf(acl) : null; + return this; + } + + public Builder defaultAcl(Iterable acl) { + this.defaultAcl = acl != null ? ImmutableList.copyOf(acl) : null; + return this; + } + + public BucketInfo build() { + checkNotNull(name); + return new BucketInfo(this); + } + } + + private BucketInfo(Builder builder) { + id = builder.id; + name = builder.name; + etag = builder.etag; + createTime = builder.createTime; + metageneration = builder.metageneration; + location = builder.location; + storageClass = builder.storageClass; + cors = builder.cors; + acl = builder.acl; + defaultAcl = builder.defaultAcl; + owner = builder.owner; + selfLink = builder.selfLink; + versioningEnabled = builder.versioningEnabled; + indexPage = builder.indexPage; + notFoundPage = builder.notFoundPage; + deleteRules = builder.deleteRules; + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public Entity owner() { + return owner; + } + + public String selfLink() { + return selfLink; + } + + public Boolean versioningEnabled() { + return Data.isNull(versioningEnabled) ? null : versioningEnabled; + } + + public String indexPage() { + return indexPage; + } + + public String notFoundPage() { + return notFoundPage; + } + + public List deleteRules() { + return deleteRules; + } + + public String etag() { + return etag; + } + + public Long createTime() { + return createTime; + } + + public Long metageneration() { + return metageneration; + } + + public Location location() { + return location == null || Data.isNull(location.value) ? null : location; + } + + public StorageClass storageClass() { + return storageClass == null || Data.isNull(storageClass.value) ? null : storageClass; + } + + public List cors() { + return cors; + } + + public List acl() { + return acl; + } + + public List defaultAcl() { + return defaultAcl; + } + + public Builder toBuilder() { + return new Builder() + .name(name) + .id(id) + .createTime(createTime) + .etag(etag) + .metageneration(metageneration) + .cors(cors) + .acl(acl) + .defaultAcl(defaultAcl) + .location(location) + .storageClass(storageClass) + .owner(owner) + .selfLink(selfLink) + .versioningEnabled(versioningEnabled) + .indexPage(indexPage) + .notFoundPage(notFoundPage) + .deleteRules(deleteRules); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof BucketInfo)) { + return false; + } + return Objects.equals(toPb(), ((BucketInfo) obj).toPb()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name()) + .toString(); + } + + public static BucketInfo of(String name) { + return builder(name).build(); + } + + public static Builder builder(String name) { + return new Builder().name(name); + } + + com.google.api.services.storage.model.Bucket toPb() { + com.google.api.services.storage.model.Bucket bucketPb = + new com.google.api.services.storage.model.Bucket(); + bucketPb.setId(id); + bucketPb.setName(name); + bucketPb.setEtag(etag); + if (createTime != null) { + bucketPb.setTimeCreated(new DateTime(createTime)); + } + if (metageneration != null) { + bucketPb.setMetageneration(metageneration); + } + if (location != null) { + bucketPb.setLocation(location.value()); + } + if (storageClass != null) { + bucketPb.setStorageClass(storageClass.value()); + } + if (cors != null) { + bucketPb.setCors(transform(cors, Cors.TO_PB_FUNCTION)); + } + if (acl != null) { + bucketPb.setAcl(transform(acl, new Function() { + @Override + public BucketAccessControl apply(Acl acl) { + return acl.toBucketPb(); + } + })); + } + if (defaultAcl != null) { + bucketPb.setDefaultObjectAcl(transform(defaultAcl, new Function() { + @Override + public ObjectAccessControl apply(Acl acl) { + return acl.toObjectPb(); + } + })); + } + if (owner != null) { + bucketPb.setOwner(new Owner().setEntity(owner.toPb())); + } + bucketPb.setSelfLink(selfLink); + if (versioningEnabled != null) { + bucketPb.setVersioning(new Versioning().setEnabled(versioningEnabled)); + } + if (indexPage != null || notFoundPage != null) { + Website website = new Website(); + website.setMainPageSuffix(indexPage); + website.setNotFoundPage(notFoundPage); + bucketPb.setWebsite(website); + } + if (deleteRules != null) { + Lifecycle lifecycle = new Lifecycle(); + lifecycle.setRule(transform(deleteRules, new Function() { + @Override + public Rule apply(DeleteRule deleteRule) { + return deleteRule.toPb(); + } + })); + bucketPb.setLifecycle(lifecycle); + } + return bucketPb; + } + + static BucketInfo fromPb(com.google.api.services.storage.model.Bucket bucketPb) { + Builder builder = new Builder() + .name(bucketPb.getName()) + .id(bucketPb.getId()) + .etag(bucketPb.getEtag()) + .metageneration(bucketPb.getMetageneration()) + .createTime(bucketPb.getTimeCreated().getValue()) + .location(Location.of(bucketPb.getLocation())) + .selfLink(bucketPb.getSelfLink()); + if (bucketPb.getStorageClass() != null) { + builder.storageClass(StorageClass.of(bucketPb.getStorageClass())); + } + if (bucketPb.getCors() != null) { + builder.cors(transform(bucketPb.getCors(), Cors.FROM_PB_FUNCTION)); + } + if (bucketPb.getAcl() != null) { + builder.acl(transform(bucketPb.getAcl(), new Function() { + @Override + public Acl apply(BucketAccessControl bucketAccessControl) { + return Acl.fromPb(bucketAccessControl); + } + })); + } + if (bucketPb.getDefaultObjectAcl() != null) { + builder.defaultAcl(transform(bucketPb.getDefaultObjectAcl(), + new Function() { + @Override + public Acl apply(ObjectAccessControl objectAccessControl) { + return Acl.fromPb(objectAccessControl); + } + })); + } + if (bucketPb.getOwner() != null) { + builder.owner(Entity.fromPb(bucketPb.getOwner().getEntity())); + } + if (bucketPb.getVersioning() != null) { + builder.versioningEnabled(bucketPb.getVersioning().getEnabled()); + } + Website website = bucketPb.getWebsite(); + if (website != null) { + builder.indexPage(website.getMainPageSuffix()); + builder.notFoundPage(website.getNotFoundPage()); + } + if (bucketPb.getLifecycle() != null && bucketPb.getLifecycle().getRule() != null) { + builder.deleteRules(transform(bucketPb.getLifecycle().getRule(), + new Function() { + @Override + public DeleteRule apply(Rule rule) { + return DeleteRule.fromPb(rule); + } + })); + } + return builder.build(); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Cors.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Cors.java new file mode 100644 index 000000000000..ce8cfb95b6e9 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Cors.java @@ -0,0 +1,234 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; +import static com.google.common.collect.Lists.newArrayList; + +import com.google.api.services.storage.model.Bucket; +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.collect.ImmutableList; + +import java.io.Serializable; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; + +/** + * Cross-Origin Resource Sharing (CORS) configuration for a bucket. + */ +public final class Cors implements Serializable { + + private static final long serialVersionUID = -8637770919343335655L; + + static final Function FROM_PB_FUNCTION = new Function() { + @Override + public Cors apply(Bucket.Cors pb) { + return Cors.fromPb(pb); + } + }; + + static final Function TO_PB_FUNCTION = new Function() { + @Override + public Bucket.Cors apply(Cors cors) { + return cors.toPb(); + } + }; + + private final Integer maxAgeSeconds; + private final ImmutableList methods; + private final ImmutableList origins; + private final ImmutableList responseHeaders; + + public static final class Origin implements Serializable { + + private static final long serialVersionUID = -4447958124895577993L; + private static final String ANY_URI = "*"; + private final String value; + + private static final Origin ANY = new Origin(ANY_URI); + + private Origin(String value) { + this.value = checkNotNull(value); + } + + public static Origin any() { + return ANY; + } + + public static Origin of(String scheme, String host, int port) { + try { + return of(new URI(scheme, null, host, port, null, null, null).toString()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public static Origin of(String value) { + if (ANY_URI.equals(value)) { + return any(); + } + return new Origin(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Origin)) { + return false; + } + return value.equals(((Origin)obj).value); + } + + @Override + public String toString() { + return value(); + } + + public String value() { + return value; + } + } + + public static final class Builder { + + private Integer maxAgeSeconds; + private ImmutableList methods; + private ImmutableList origins; + private ImmutableList responseHeaders; + + private Builder() {} + + public Builder maxAgeSeconds(Integer maxAgeSeconds) { + this.maxAgeSeconds = maxAgeSeconds; + return this; + } + + public Builder methods(Iterable methods) { + this.methods = methods != null ? ImmutableList.copyOf(methods) : null; + return this; + } + + public Builder origins(Iterable origins) { + this.origins = origins != null ? ImmutableList.copyOf(origins) : null; + return this; + } + + public Builder responseHeaders(Iterable headers) { + this.responseHeaders = headers != null ? ImmutableList.copyOf(headers) : null; + return this; + } + + public Cors build() { + return new Cors(this); + } + } + + private Cors(Builder builder) { + this.maxAgeSeconds = builder.maxAgeSeconds; + this.methods = builder.methods; + this.origins = builder.origins; + this.responseHeaders = builder.responseHeaders; + } + + public Integer maxAgeSeconds() { + return maxAgeSeconds; + } + + public List methods() { + return methods; + } + + public List origins() { + return origins; + } + + public List responseHeaders() { + return responseHeaders; + } + + public Builder toBuilder() { + return builder() + .maxAgeSeconds(maxAgeSeconds) + .methods(methods) + .origins(origins) + .responseHeaders(responseHeaders); + } + + @Override + public int hashCode() { + return Objects.hash(maxAgeSeconds, methods, origins, responseHeaders); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Cors)) { + return false; + } + Cors other = (Cors) obj; + return Objects.equals(maxAgeSeconds, other.maxAgeSeconds) + && Objects.equals(methods, other.methods) + && Objects.equals(origins, other.origins) + && Objects.equals(responseHeaders, other.responseHeaders); + } + + public static Builder builder() { + return new Builder(); + } + + Bucket.Cors toPb() { + Bucket.Cors pb = new Bucket.Cors(); + pb.setMaxAgeSeconds(maxAgeSeconds); + pb.setResponseHeader(responseHeaders); + if (methods != null) { + pb.setMethod(newArrayList(transform(methods, Functions.toStringFunction()))); + } + if (origins != null) { + pb.setOrigin(newArrayList(transform(origins, Functions.toStringFunction()))); + } + return pb; + } + + static Cors fromPb(Bucket.Cors cors) { + Builder builder = builder().maxAgeSeconds(cors.getMaxAgeSeconds()); + if (cors.getMethod() != null) { + builder.methods(transform(cors.getMethod(), new Function() { + @Override + public HttpMethod apply(String name) { + return HttpMethod.valueOf(name.toUpperCase()); + } + })); + } + if (cors.getOrigin() != null) { + builder.origins(transform(cors.getOrigin(), new Function() { + @Override + public Origin apply(String value) { + return Origin.of(value); + } + })); + } + builder.responseHeaders(cors.getResponseHeader()); + return builder.build(); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/HttpMethod.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/HttpMethod.java new file mode 100644 index 000000000000..9d7944140915 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/HttpMethod.java @@ -0,0 +1,24 @@ +/* + * Copyright 2015 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.gcloud.storage; + +/** + * Http method supported by Storage service. + */ +public enum HttpMethod { + GET, HEAD, PUT, POST, DELETE +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/ListResult.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/ListResult.java new file mode 100644 index 000000000000..f9319f903760 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/ListResult.java @@ -0,0 +1,82 @@ +/* + * Copyright 2015 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.gcloud.storage; + + +import java.io.Serializable; +import java.util.Collections; +import java.util.Iterator; +import java.util.Objects; + +/** + * Google Cloud storage list result. + */ +public final class ListResult implements Iterable, Serializable { + + private static final long serialVersionUID = -6937287874908527950L; + + private final String cursor; + private final Iterable results; + private final NextPageFetcher pageFetcher; + + interface NextPageFetcher extends Serializable { + ListResult nextPage(); + } + + public ListResult(NextPageFetcher pageFetcher, String cursor, Iterable results) { + this.pageFetcher = pageFetcher; + this.cursor = cursor; + this.results = results; + } + + /** + * Returns the cursor for the nextPage or {@code null} if no more results. + */ + public String nextPageCursor() { + return cursor; + } + + /** + * Returns the results of the nextPage or {@code null} if no more result. + */ + public ListResult nextPage() { + if (cursor == null || pageFetcher == null) { + return null; + } + return pageFetcher.nextPage(); + } + + @Override + public Iterator iterator() { + return results == null ? Collections.emptyIterator() : results.iterator(); + } + + @Override + public int hashCode() { + return Objects.hash(cursor, results); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ListResult)) { + return false; + } + ListResult other = (ListResult) obj; + return Objects.equals(cursor, other.cursor) + && Objects.equals(results, other.results); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/ObjectId.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/ObjectId.java new file mode 100644 index 000000000000..7e079b7c6b3d --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/ObjectId.java @@ -0,0 +1,133 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import java.net.URI; +import java.util.Objects; + +/** + * Identifier for a stored object. + */ +public class ObjectId { + + private final String bucketName; + private final String objectName; + private final URI uri; + + public ObjectId(String uri) { + this(URI.create(uri)); + } + + /** + * Constructs an ObjectId from a {@code gsutil} style URI. + * + * @param uri the object's URI + */ + public ObjectId(URI uri) { + Objects.requireNonNull(uri, "uri must not be null"); + this.uri = uri; + if (uri.isAbsolute()) { + if (!"gs".equals(uri.getScheme())) { + throw new IllegalArgumentException("uri must use gs: scheme"); + } + if (uri.getPath() == null || uri.getPath().length() <= 1) { + throw new IllegalArgumentException("uri must have a path: " + uri); + } + bucketName = uri.getAuthority(); + objectName = uri.getPath().substring(1); + } else { + this.bucketName = null; + this.objectName = uri.getPath(); + } + } + + /** + * Construct an absolute ObjectId from a bucket and object name. + * + * @param bucketName name of the bucket + * @param objectName name of the object; must be a path relative to the bucket i.e. must not start with '/' + */ + public ObjectId(String bucketName, String objectName) { + Objects.requireNonNull(bucketName, "bucketName must not be null"); + if (bucketName.isEmpty()) { + throw new IllegalArgumentException("bucketName must not be empty"); + } + Objects.requireNonNull(objectName, "objectName must not be null"); + if (objectName.isEmpty()) { + throw new IllegalArgumentException("objectName must not be empty"); + } + if (objectName.charAt(0) == '/') { + throw new IllegalArgumentException("objectName must be a relative path"); + } + this.bucketName = bucketName; + this.objectName = objectName; + this.uri = URI.create("gs://" + bucketName + "/" + objectName); + } + + /** + * Returns the name of the bucket containing this object. May be null for object ids that solely define an + * object name independent of bucket. + * + * @return the name of the bucket containing this object; may be null + */ + public String getBucketName() { + return bucketName; + } + + /** + * Returns the name of the object within the bucket. + * @return the name of the object within the bucket + */ + public String getObjectName() { + return objectName; + } + + /** + * Returns the {@code gsutil} URI for this object. + * + *

If a bucketName is defined this will return an absolute URI of the form {@code gs://bucketName/objectName}, + * otherwise the objectName will be converted to a relative URI. + * + * @return the uri for this object + */ + public URI toURI() { + return uri; + } + + /** + * Returns this objects URI as a String. + * @return this objects URI as a String + */ + @Override + public String toString() { + return uri.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ObjectId other = (ObjectId) o; + return Objects.equals(bucketName, other.bucketName) && + Objects.equals(objectName, other.objectName); + } + + @Override + public int hashCode() { + return Objects.hash(bucketName, objectName); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Option.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Option.java new file mode 100644 index 000000000000..798db688c8ec --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Option.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.MoreObjects; +import com.google.gcloud.spi.StorageRpc; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Base class for Storage operation option + */ +class Option implements Serializable { + + private static final long serialVersionUID = -73199088766477208L; + + private final StorageRpc.Option rpcOption; + private final Object value; + + Option(StorageRpc.Option rpcOption, Object value) { + this.rpcOption = checkNotNull(rpcOption); + this.value = value; + } + + StorageRpc.Option rpcOption() { + return rpcOption; + } + + Object value() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Option)) { + return false; + } + Option other = (Option) obj; + return Objects.equals(rpcOption, other.rpcOption) + && Objects.equals(value, other.value); + } + + @Override + public int hashCode() { + return Objects.hash(rpcOption, value); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", rpcOption.value()) + .add("value", value) + .toString(); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java new file mode 100644 index 000000000000..6427bfaccf58 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java @@ -0,0 +1,605 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.gcloud.AuthCredentials.ServiceAccountAuthCredentials; +import com.google.gcloud.Service; +import com.google.gcloud.spi.StorageRpc; + +import java.io.Serializable; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * An interface for Google Cloud Storage. + * + * @see Google Cloud Storage + */ +public interface Storage extends Service { + + enum PredefinedAcl { + AUTHENTICATED_READ("authenticatedRead"), + ALL_AUTHENTICATED_USERS("allAuthenticatedUsers"), + PRIVATE("private"), + PROJECT_PRIVATE("projectPrivate"), + PUBLIC_READ("publicRead"), + PUBLIC_READ_WRITE("publicReadWrite"), + BUCKET_OWNER_READ("bucketOwnerRead"), + BUCKET_OWNER_FULL_CONTROL("bucketOwnerFullControl"); + + private final String entry; + + PredefinedAcl(String entry) { + this.entry = entry; + } + + String entry() { + return entry; + } + } + + class BucketTargetOption extends Option { + + private static final long serialVersionUID = -5880204616982900975L; + + private BucketTargetOption(StorageRpc.Option rpcOption, Object value) { + super(rpcOption, value); + } + + private BucketTargetOption(StorageRpc.Option rpcOption) { + this(rpcOption, null); + } + + public static BucketTargetOption predefinedAcl(PredefinedAcl acl) { + return new BucketTargetOption(StorageRpc.Option.PREDEFINED_ACL, acl.entry()); + } + + public static BucketTargetOption predefinedDefaultObjectAcl(PredefinedAcl acl) { + return new BucketTargetOption(StorageRpc.Option.PREDEFINED_DEFAULT_OBJECT_ACL, acl.entry()); + } + + public static BucketTargetOption metagenerationMatch() { + return new BucketTargetOption(StorageRpc.Option.IF_METAGENERATION_MATCH); + } + + public static BucketTargetOption metagenerationNotMatch() { + return new BucketTargetOption(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH); + } + } + + class BucketSourceOption extends Option { + + private static final long serialVersionUID = 5185657617120212117L; + + private BucketSourceOption(StorageRpc.Option rpcOption, long metageneration) { + super(rpcOption, metageneration); + } + + public static BucketSourceOption metagenerationMatch(long metageneration) { + return new BucketSourceOption(StorageRpc.Option.IF_METAGENERATION_MATCH, metageneration); + } + + public static BucketSourceOption metagenerationNotMatch(long metageneration) { + return new BucketSourceOption(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH, metageneration); + } + } + + class BlobTargetOption extends Option { + + private static final long serialVersionUID = 214616862061934846L; + + private BlobTargetOption(StorageRpc.Option rpcOption, Object value) { + super(rpcOption, value); + } + + private BlobTargetOption(StorageRpc.Option rpcOption) { + this(rpcOption, null); + } + + public static BlobTargetOption predefinedAcl(PredefinedAcl acl) { + return new BlobTargetOption(StorageRpc.Option.PREDEFINED_ACL, acl.entry()); + } + + public static BlobTargetOption doesNotExist() { + return new BlobTargetOption(StorageRpc.Option.IF_GENERATION_MATCH, 0L); + } + + public static BlobTargetOption generationMatch() { + return new BlobTargetOption(StorageRpc.Option.IF_GENERATION_MATCH); + } + + public static BlobTargetOption generationNotMatch() { + return new BlobTargetOption(StorageRpc.Option.IF_GENERATION_NOT_MATCH); + } + + public static BlobTargetOption metagenerationMatch() { + return new BlobTargetOption(StorageRpc.Option.IF_METAGENERATION_MATCH); + } + + public static BlobTargetOption metagenerationNotMatch() { + return new BlobTargetOption(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH); + } + } + + class BlobSourceOption extends Option { + + private static final long serialVersionUID = -3712768261070182991L; + + private BlobSourceOption(StorageRpc.Option rpcOption, long value) { + super(rpcOption, value); + } + + public static BlobSourceOption generationMatch(long generation) { + return new BlobSourceOption(StorageRpc.Option.IF_GENERATION_MATCH, generation); + } + + public static BlobSourceOption generationNotMatch(long generation) { + return new BlobSourceOption(StorageRpc.Option.IF_GENERATION_NOT_MATCH, generation); + } + + public static BlobSourceOption metagenerationMatch(long metageneration) { + return new BlobSourceOption(StorageRpc.Option.IF_METAGENERATION_MATCH, metageneration); + } + + public static BlobSourceOption metagenerationNotMatch(long metageneration) { + return new BlobSourceOption(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH, metageneration); + } + } + + class BucketListOption extends Option { + + private static final long serialVersionUID = 8754017079673290353L; + + private BucketListOption(StorageRpc.Option option, Object value) { + super(option, value); + } + + public static BucketListOption maxResults(long maxResults) { + return new BucketListOption(StorageRpc.Option.MAX_RESULTS, maxResults); + } + + public static BucketListOption startPageToken(String pageToken) { + return new BucketListOption(StorageRpc.Option.PAGE_TOKEN, pageToken); + } + + public static BucketListOption prefix(String prefix) { + return new BucketListOption(StorageRpc.Option.PREFIX, prefix); + } + } + + class BlobListOption extends Option { + + private static final long serialVersionUID = 9083383524788661294L; + + private BlobListOption(StorageRpc.Option option, Object value) { + super(option, value); + } + + public static BlobListOption maxResults(long maxResults) { + return new BlobListOption(StorageRpc.Option.MAX_RESULTS, maxResults); + } + + public static BlobListOption startPageToken(String pageToken) { + return new BlobListOption(StorageRpc.Option.PAGE_TOKEN, pageToken); + } + + public static BlobListOption prefix(String prefix) { + return new BlobListOption(StorageRpc.Option.PREFIX, prefix); + } + + public static BlobListOption recursive(boolean recursive) { + return new BlobListOption(StorageRpc.Option.DELIMITER, recursive); + } + } + + class SignUrlOption implements Serializable { + + private static final long serialVersionUID = 7850569877451099267L; + + private final Option option; + private final Object value; + + enum Option { + HTTP_METHOD, CONTENT_TYPE, MD5, SERVICE_ACCOUNT_CRED; + } + + private SignUrlOption(Option option, Object value) { + this.option = option; + this.value = value; + } + + Option option() { + return option; + } + + Object value() { + return value; + } + + /** + * The HTTP method to be used with the signed URL. + */ + public static SignUrlOption httpMethod(HttpMethod httpMethod) { + return new SignUrlOption(Option.HTTP_METHOD, httpMethod.name()); + } + + /** + * Use it if signature should include the blob's content-type. + * When used, users of the signed URL should include the blob's content-type with their request. + */ + public static SignUrlOption withContentType() { + return new SignUrlOption(Option.CONTENT_TYPE, true); + } + + /** + * Use it if signature should include the blob's md5. + * When used, users of the signed URL should include the blob's md5 with their request. + */ + public static SignUrlOption withMd5() { + return new SignUrlOption(Option.MD5, true); + } + + /** + * Service account credentials which are used for signing the URL. + * If not provided an attempt will be made to get it from the environment. + * + * @see Service account + */ + public static SignUrlOption serviceAccount(ServiceAccountAuthCredentials credentials) { + return new SignUrlOption(Option.SERVICE_ACCOUNT_CRED, credentials); + } + } + + class ComposeRequest implements Serializable { + + private static final long serialVersionUID = -7385681353748590911L; + + private final List sourceBlobs; + private final BlobInfo target; + private final List targetOptions; + + public static class SourceBlob implements Serializable { + + private static final long serialVersionUID = 4094962795951990439L; + + final String name; + final Long generation; + + SourceBlob(String name) { + this(name, null); + } + + SourceBlob(String name, Long generation) { + this.name = name; + this.generation = generation; + } + + public String name() { + return name; + } + + public Long generation() { + return generation; + } + } + + public static class Builder { + + private final List sourceBlobs = new LinkedList<>(); + private BlobInfo target; + private final Set targetOptions = new LinkedHashSet<>(); + + public Builder addSource(Iterable blobs) { + for (String blob : blobs) { + sourceBlobs.add(new SourceBlob(blob)); + } + return this; + } + + public Builder addSource(String... blobs) { + return addSource(Arrays.asList(blobs)); + } + + /** + * Add a source with a specific generation to match. + */ + public Builder addSource(String blob, long generation) { + sourceBlobs.add(new SourceBlob(blob, generation)); + return this; + } + + public Builder target(BlobInfo target) { + this.target = target; + return this; + } + + public Builder targetOptions(BlobTargetOption... options) { + Collections.addAll(targetOptions, options); + return this; + } + + public ComposeRequest build() { + checkArgument(!sourceBlobs.isEmpty()); + checkNotNull(target); + return new ComposeRequest(this); + } + } + + private ComposeRequest(Builder builder) { + sourceBlobs = ImmutableList.copyOf(builder.sourceBlobs); + target = builder.target; + targetOptions = ImmutableList.copyOf(builder.targetOptions); + } + + public List sourceBlobs() { + return sourceBlobs; + } + + public BlobInfo target() { + return target; + } + + public List targetOptions() { + return targetOptions; + } + + public static ComposeRequest of(Iterable sources, BlobInfo target) { + return builder().target(target).addSource(sources).build(); + } + + public static ComposeRequest of(String bucket, Iterable sources, String target) { + return of(sources, BlobInfo.of(bucket, target)); + } + + public static Builder builder() { + return new Builder(); + } + } + + class CopyRequest implements Serializable { + + private static final long serialVersionUID = -2606508373751748775L; + + private final String sourceBucket; + private final String sourceBlob; + private final List sourceOptions; + private final BlobInfo target; + private final List targetOptions; + + public static class Builder { + + private String sourceBucket; + private String sourceBlob; + private final Set sourceOptions = new LinkedHashSet<>(); + private BlobInfo target; + private final Set targetOptions = new LinkedHashSet<>(); + + public Builder source(String bucket, String blob) { + this.sourceBucket = bucket; + this.sourceBlob = blob; + return this; + } + + public Builder sourceOptions(BlobSourceOption... options) { + Collections.addAll(sourceOptions, options); + return this; + } + + public Builder target(BlobInfo target) { + this.target = target; + return this; + } + + public Builder targetOptions(BlobTargetOption... options) { + Collections.addAll(targetOptions, options); + return this; + } + + public CopyRequest build() { + checkNotNull(sourceBucket); + checkNotNull(sourceBlob); + checkNotNull(target); + return new CopyRequest(this); + } + } + + private CopyRequest(Builder builder) { + sourceBucket = checkNotNull(builder.sourceBucket); + sourceBlob = checkNotNull(builder.sourceBlob); + sourceOptions = ImmutableList.copyOf(builder.sourceOptions); + target = checkNotNull(builder.target); + targetOptions = ImmutableList.copyOf(builder.targetOptions); + } + + public String sourceBucket() { + return sourceBucket; + } + + public String sourceBlob() { + return sourceBlob; + } + + public List sourceOptions() { + return sourceOptions; + } + + public BlobInfo target() { + return target; + } + + public List targetOptions() { + return targetOptions; + } + + public static CopyRequest of(String sourceBucket, String sourceBlob, BlobInfo target) { + return builder().source(sourceBucket, sourceBlob).target(target).build(); + } + + public static CopyRequest of(String sourceBucket, String sourceBlob, String targetBlob) { + return of(sourceBucket, sourceBlob, BlobInfo.of(sourceBucket, targetBlob)); + } + + public static Builder builder() { + return new Builder(); + } + } + + /** + * Create a new bucket. + * + * @return a complete bucket information. + * @throws StorageException upon failure + */ + BucketInfo create(BucketInfo bucketInfo, BucketTargetOption... options); + + /** + * Create a new blob. + * + * @return a complete blob information. + * @throws StorageException upon failure + */ + BlobInfo create(BlobInfo blobInfo, byte[] content, BlobTargetOption... options); + + /** + * Return the requested bucket or {@code null} if not found. + * + * @throws StorageException upon failure + */ + BucketInfo get(String bucket, BucketSourceOption... options); + + /** + * Return the requested blob or {@code null} if not found. + * + * @throws StorageException upon failure + */ + BlobInfo get(String bucket, String blob, BlobSourceOption... options); + + /** + * List the project's buckets. + * + * @throws StorageException upon failure + */ + ListResult list(BucketListOption... options); + + /** + * List the bucket's blobs. + * + * @throws StorageException upon failure + */ + ListResult list(String bucket, BlobListOption... options); + + /** + * Update bucket information. + * + * @return the updated bucket + * @throws StorageException upon failure + */ + BucketInfo update(BucketInfo bucketInfo, BucketTargetOption... options); + + /** + * Update blob information. + * + * @return the updated blob + * @throws StorageException upon failure + */ + BlobInfo update(BlobInfo blobInfo, BlobTargetOption... options); + + /** + * Delete the requested bucket. + * + * @return true if bucket was deleted + * @throws StorageException upon failure + */ + boolean delete(String bucket, BucketSourceOption... options); + + /** + * Delete the requested blob. + * + * @return true if blob was deleted + * @throws StorageException upon failure + */ + boolean delete(String bucket, String blob, BlobSourceOption... options); + + /** + * Send a compose request. + * + * @return the composed blob. + * @throws StorageException upon failure + */ + BlobInfo compose(ComposeRequest composeRequest); + + /** + * Send a copy request. + * + * @return the copied blob. + * @throws StorageException upon failure + */ + BlobInfo copy(CopyRequest copyRequest); + + /** + * Reads all the bytes from a blob. + * + * @return the blob's content. + * @throws StorageException upon failure + */ + byte[] readAllBytes(String bucket, String blob, BlobSourceOption... options); + + /** + * Send a batch request. + * + * @return the batch response + * @throws StorageException upon failure + */ + BatchResponse apply(BatchRequest batchRequest); + + /** + * Return a channel for reading the blob's content. + * + * @throws StorageException upon failure + */ + BlobReadChannel reader(String bucket, String blob, BlobSourceOption... options); + + /** + * Create a blob and return a channel for writing its content. + * + * @throws StorageException upon failure + */ + BlobWriteChannel writer(BlobInfo blobInfo, BlobTargetOption... options); + + /** + * Generates a signed URL for a blob. + * If you have a blob that you want to allow access to for a fixed + * amount of time, you can use this method to generate a URL that + * is only valid within a certain time period. + * This is particularly useful if you don't want publicly + * accessible blobs, but don't want to require users to explicitly log in. + * + * @param blobInfo the blob associated with the signed url + * @param expirationTimeInSeconds the signed URL expiration (using epoch time) + * @see Signed-URLs + */ + URL signUrl(BlobInfo blobInfo, long expirationTimeInSeconds, SignUrlOption... options); +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageException.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageException.java new file mode 100644 index 000000000000..9bf073bef6b4 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageException.java @@ -0,0 +1 @@ +/* * Copyright 2015 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.gcloud.storage; /** * Storage service exception. * * @see Google Cloud * Storage error codes */ public class StorageException extends RuntimeException { private static final long serialVersionUID = -3748432005065428084L; private final int code; private final boolean retryable; public StorageException(int code, String message, boolean retryable) { super(message); this.code = code; this.retryable = retryable; } /** * Returns the code associated with this exception. */ public int code() { return code; } public boolean retryable() { return retryable; } } \ No newline at end of file diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageFactory.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageFactory.java new file mode 100644 index 000000000000..e269f0c9d92b --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 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.gcloud.storage; + + +/** + * A base class for Storage factories. + */ +public abstract class StorageFactory { + + private static final StorageFactory INSTANCE = new StorageFactory() { + @Override + public Storage get(StorageOptions options) { + return new StorageImpl(options); + } + }; + + /** + * Returns the default factory instance. + */ + public static StorageFactory instance() { + return INSTANCE; + } + + /** + * Returns a {@code Storage} service for the given options. + */ + public abstract Storage get(StorageOptions options); +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java new file mode 100644 index 000000000000..6eb8e3359a4d --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java @@ -0,0 +1,568 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.gcloud.RetryHelper.runWithRetries; +import static com.google.gcloud.spi.StorageRpc.Option.DELIMITER; +import static com.google.gcloud.spi.StorageRpc.Option.IF_GENERATION_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_GENERATION_NOT_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_METAGENERATION_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_METAGENERATION_NOT_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_SOURCE_GENERATION_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_SOURCE_GENERATION_NOT_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_SOURCE_METAGENERATION_MATCH; +import static com.google.gcloud.spi.StorageRpc.Option.IF_SOURCE_METAGENERATION_NOT_MATCH; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.api.services.storage.model.StorageObject; +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.io.BaseEncoding; +import com.google.common.primitives.Ints; +import com.google.gcloud.AuthCredentials.ServiceAccountAuthCredentials; +import com.google.gcloud.BaseService; +import com.google.gcloud.ExceptionHandler; +import com.google.gcloud.ExceptionHandler.Interceptor; +import com.google.gcloud.spi.StorageRpc; +import com.google.gcloud.spi.StorageRpc.Tuple; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; + +final class StorageImpl extends BaseService implements Storage { + + private static final Interceptor EXCEPTION_HANDLER_INTERCEPTOR = new Interceptor() { + + private static final long serialVersionUID = -7758580330857881124L; + + @Override + public RetryResult afterEval(Exception exception, RetryResult retryResult) { + return null; + } + + @Override + public RetryResult beforeEval(Exception exception) { + if (exception instanceof StorageException) { + boolean retriable = ((StorageException) exception).retryable(); + return retriable ? Interceptor.RetryResult.RETRY : Interceptor.RetryResult.ABORT; + } + return null; + } + }; + static final ExceptionHandler EXCEPTION_HANDLER = ExceptionHandler.builder() + .abortOn(RuntimeException.class).interceptor(EXCEPTION_HANDLER_INTERCEPTOR).build(); + private static final byte[] EMPTY_BYTE_ARRAY = {}; + + private final StorageRpc storageRpc; + + StorageImpl(StorageOptions options) { + super(options); + storageRpc = options.storageRpc(); + // todo: configure timeouts - https://developers.google.com/api-client-library/java/google-api-java-client/errors + // todo: provide rewrite - https://cloud.google.com/storage/docs/json_api/v1/objects/rewrite + // todo: check if we need to expose https://cloud.google.com/storage/docs/json_api/v1/bucketAccessControls/insert vs using bucket update/patch + } + + @Override + public BucketInfo create(BucketInfo bucketInfo, BucketTargetOption... options) { + final com.google.api.services.storage.model.Bucket bucketPb = bucketInfo.toPb(); + final Map optionsMap = optionMap(bucketInfo, options); + return BucketInfo.fromPb(runWithRetries( + new Callable() { + @Override + public com.google.api.services.storage.model.Bucket call() { + return storageRpc.create(bucketPb, optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER)); + } + + @Override + public BlobInfo create(BlobInfo blobInfo, final byte[] content, BlobTargetOption... options) { + final StorageObject blobPb = blobInfo.toPb(); + final Map optionsMap = optionMap(blobInfo, options); + return BlobInfo.fromPb(runWithRetries(new Callable() { + @Override + public StorageObject call() { + return storageRpc.create(blobPb, firstNonNull(content, EMPTY_BYTE_ARRAY), optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER)); + } + + @Override + public BucketInfo get(String bucket, BucketSourceOption... options) { + final com.google.api.services.storage.model.Bucket bucketPb = BucketInfo.of(bucket).toPb(); + final Map optionsMap = optionMap(options); + com.google.api.services.storage.model.Bucket answer = runWithRetries( + new Callable() { + @Override + public com.google.api.services.storage.model.Bucket call() { + try { + return storageRpc.get(bucketPb, optionsMap); + } catch (StorageException ex) { + if (ex.code() == HTTP_NOT_FOUND) { + return null; + } + throw ex; + } + } + }, options().retryParams(), EXCEPTION_HANDLER); + return answer == null ? null : BucketInfo.fromPb(answer); + } + + @Override + public BlobInfo get(String bucket, String blob, BlobSourceOption... options) { + final StorageObject storedObject = BlobInfo.of(bucket, blob).toPb(); + final Map optionsMap = optionMap(options); + StorageObject storageObject = runWithRetries(new Callable() { + @Override + public StorageObject call() { + try { + return storageRpc.get(storedObject, optionsMap); + } catch (StorageException ex) { + if (ex.code() == HTTP_NOT_FOUND) { + return null; + } + throw ex; + } + } + }, options().retryParams(), EXCEPTION_HANDLER); + return storageObject == null ? null : BlobInfo.fromPb(storageObject); + } + + private static abstract class BasePageFetcher + implements ListResult.NextPageFetcher { + + private static final long serialVersionUID = 8236329004030295223L; + protected final Map requestOptions; + protected final StorageOptions serviceOptions; + + BasePageFetcher(StorageOptions serviceOptions, String cursor, + Map optionMap) { + this.serviceOptions = serviceOptions; + ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.put(StorageRpc.Option.PAGE_TOKEN, cursor); + for (Map.Entry option : optionMap.entrySet()) { + if (option.getKey() != StorageRpc.Option.PAGE_TOKEN) { + builder.put(option.getKey(), option.getValue()); + } + } + this.requestOptions = builder.build(); + } + } + + private static class BucketPageFetcher extends BasePageFetcher { + + private static final long serialVersionUID = -5490616010200159174L; + + BucketPageFetcher(StorageOptions serviceOptions, String cursor, + Map optionMap) { + super(serviceOptions, cursor, optionMap); + } + + @Override + public ListResult nextPage() { + return listBuckets(serviceOptions, requestOptions); + } + } + + private static class BlobPageFetcher extends BasePageFetcher { + + private static final long serialVersionUID = -5490616010200159174L; + private final String bucket; + + BlobPageFetcher(String bucket, StorageOptions serviceOptions, String cursor, + Map optionMap) { + super(serviceOptions, cursor, optionMap); + this.bucket = bucket; + } + + @Override + public ListResult nextPage() { + return listBlobs(bucket, serviceOptions, requestOptions); + } + } + + @Override + public ListResult list(BucketListOption... options) { + return listBuckets(options(), optionMap(options)); + } + + private static ListResult listBuckets(final StorageOptions serviceOptions, + final Map optionsMap) { + Tuple> result = runWithRetries( + new Callable>>() { + @Override + public Tuple> call() { + return serviceOptions.storageRpc().list(optionsMap); + } + }, serviceOptions.retryParams(), EXCEPTION_HANDLER); + String cursor = result.x(); + return new ListResult<>(new BucketPageFetcher(serviceOptions, cursor, optionsMap), cursor, + Iterables.transform(result.y(), + new Function() { + @Override + public BucketInfo apply(com.google.api.services.storage.model.Bucket bucketPb) { + return BucketInfo.fromPb(bucketPb); + } + })); + } + + @Override + public ListResult list(final String bucket, BlobListOption... options) { + return listBlobs(bucket, options(), optionMap(options)); + } + + private static ListResult listBlobs(final String bucket, + final StorageOptions serviceOptions, final Map optionsMap) { + Tuple> result = runWithRetries( + new Callable>>() { + @Override + public Tuple> call() { + return serviceOptions.storageRpc().list(bucket, optionsMap); + } + }, serviceOptions.retryParams(), EXCEPTION_HANDLER); + String cursor = result.x(); + return new ListResult<>(new BlobPageFetcher(bucket, serviceOptions, cursor, optionsMap), cursor, + Iterables.transform(result.y(), + new Function() { + @Override + public BlobInfo apply(StorageObject storageObject) { + return BlobInfo.fromPb(storageObject); + } + })); + } + + @Override + public BucketInfo update(BucketInfo bucketInfo, BucketTargetOption... options) { + final com.google.api.services.storage.model.Bucket bucketPb = bucketInfo.toPb(); + final Map optionsMap = optionMap(bucketInfo, options); + return BucketInfo.fromPb(runWithRetries( + new Callable() { + @Override + public com.google.api.services.storage.model.Bucket call() { + return storageRpc.patch(bucketPb, optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER)); + } + + @Override + public BlobInfo update(BlobInfo blobInfo, BlobTargetOption... options) { + final StorageObject storageObject = blobInfo.toPb(); + final Map optionsMap = optionMap(blobInfo, options); + return BlobInfo.fromPb(runWithRetries(new Callable() { + @Override + public StorageObject call() { + return storageRpc.patch(storageObject, optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER)); + } + + @Override + public boolean delete(String bucket, BucketSourceOption... options) { + final com.google.api.services.storage.model.Bucket bucketPb = BucketInfo.of(bucket).toPb(); + final Map optionsMap = optionMap(options); + return runWithRetries(new Callable() { + @Override + public Boolean call() { + return storageRpc.delete(bucketPb, optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER); + } + + @Override + public boolean delete(String bucket, String blob, BlobSourceOption... options) { + final StorageObject storageObject = BlobInfo.of(bucket, blob).toPb(); + final Map optionsMap = optionMap(options); + return runWithRetries(new Callable() { + @Override + public Boolean call() { + return storageRpc.delete(storageObject, optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER); + } + + @Override + public BlobInfo compose(final ComposeRequest composeRequest) { + final List sources = + Lists.newArrayListWithCapacity(composeRequest.sourceBlobs().size()); + for (ComposeRequest.SourceBlob sourceBlob : composeRequest.sourceBlobs()) { + sources.add(BlobInfo.builder(composeRequest.target().bucket(), sourceBlob.name()) + .generation(sourceBlob.generation()).build().toPb()); + } + final StorageObject target = composeRequest.target().toPb(); + final Map targetOptions = optionMap(composeRequest.target().generation(), + composeRequest.target().metageneration(), composeRequest.targetOptions()); + return BlobInfo.fromPb(runWithRetries(new Callable() { + @Override + public StorageObject call() { + return storageRpc.compose(sources, target, targetOptions); + } + }, options().retryParams(), EXCEPTION_HANDLER)); + } + + @Override + public BlobInfo copy(CopyRequest copyRequest) { + final StorageObject source = + BlobInfo.of(copyRequest.sourceBucket(), copyRequest.sourceBlob()).toPb(); + copyRequest.sourceOptions(); + final Map sourceOptions = + optionMap(null, null, copyRequest.sourceOptions(), true); + final StorageObject target = copyRequest.target().toPb(); + final Map targetOptions = optionMap(copyRequest.target().generation(), + copyRequest.target().metageneration(), copyRequest.targetOptions()); + return BlobInfo.fromPb(runWithRetries(new Callable() { + @Override + public StorageObject call() { + return storageRpc.copy(source, sourceOptions, target, targetOptions); + } + }, options().retryParams(), EXCEPTION_HANDLER)); + } + + @Override + public byte[] readAllBytes(String bucket, String blob, BlobSourceOption... options) { + final StorageObject storageObject = BlobInfo.of(bucket, blob).toPb(); + final Map optionsMap = optionMap(options); + return runWithRetries(new Callable() { + @Override + public byte[] call() { + return storageRpc.load(storageObject, optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER); + } + + @Override + public BatchResponse apply(BatchRequest batchRequest) { + List>> toDelete = + Lists.newArrayListWithCapacity(batchRequest.toDelete().size()); + for (Map.Entry> entry : batchRequest.toDelete().entrySet()) { + BlobInfo blobInfo = entry.getKey(); + Map optionsMap = + optionMap(blobInfo.generation(), blobInfo.metageneration(), entry.getValue()); + StorageObject storageObject = blobInfo.toPb(); + toDelete.add(Tuple.>of(storageObject, optionsMap)); + } + List>> toUpdate = + Lists.newArrayListWithCapacity(batchRequest.toUpdate().size()); + for (Map.Entry> entry : batchRequest.toUpdate().entrySet()) { + BlobInfo blobInfo = entry.getKey(); + Map optionsMap = + optionMap(blobInfo.generation(), blobInfo.metageneration(), entry.getValue()); + toUpdate.add(Tuple.>of(blobInfo.toPb(), optionsMap)); + } + List>> toGet = + Lists.newArrayListWithCapacity(batchRequest.toGet().size()); + for (Map.Entry> entry : batchRequest.toGet().entrySet()) { + BlobInfo blobInfo = entry.getKey(); + Map optionsMap = + optionMap(blobInfo.generation(), blobInfo.metageneration(), entry.getValue()); + toGet.add(Tuple.>of(blobInfo.toPb(), optionsMap)); + } + StorageRpc.BatchResponse response = + storageRpc.batch(new StorageRpc.BatchRequest(toDelete, toUpdate, toGet)); + List> deletes = transformBatchResult( + toDelete, response.deletes, Functions.identity()); + List> updates = transformBatchResult( + toUpdate, response.updates, BlobInfo.FROM_PB_FUNCTION); + List> gets = transformBatchResult( + toGet, response.gets, BlobInfo.FROM_PB_FUNCTION, HTTP_NOT_FOUND); + return new BatchResponse(deletes, updates, gets); + } + + private List> transformBatchResult( + Iterable>> request, + Map> results, Function transform, + int... nullOnErrorCodes) { + Set nullOnErrorCodesSet = Sets.newHashSet(Ints.asList(nullOnErrorCodes)); + List> response = Lists.newArrayListWithCapacity(results.size()); + for (Tuple tuple : request) { + Tuple result = results.get(tuple.x()); + if (result.x() != null) { + response.add(BatchResponse.Result.of(transform.apply(result.x()))); + } else { + StorageException exception = result.y(); + if (nullOnErrorCodesSet.contains(exception.code())) { + //noinspection unchecked + response.add(BatchResponse.Result.empty()); + } else { + response.add(new BatchResponse.Result(exception)); + } + } + } + return response; + } + + @Override + public BlobReadChannel reader(String bucket, String blob, BlobSourceOption... options) { + Map optionsMap = optionMap(options); + return new BlobReadChannelImpl(options(), BlobInfo.of(bucket, blob), optionsMap); + } + + @Override + public BlobWriteChannel writer(BlobInfo blobInfo, BlobTargetOption... options) { + final Map optionsMap = optionMap(blobInfo, options); + return new BlobWriterChannelImpl(options(), blobInfo, optionsMap); + } + + @Override + public URL signUrl(BlobInfo blobInfo, long expiration, SignUrlOption... options) { + EnumMap optionMap = Maps.newEnumMap(SignUrlOption.Option.class); + for (SignUrlOption option : options) { + optionMap.put(option.option(), option.value()); + } + ServiceAccountAuthCredentials cred = + (ServiceAccountAuthCredentials) optionMap.get(SignUrlOption.Option.SERVICE_ACCOUNT_CRED); + if (cred == null) { + checkArgument(options().authCredentials() instanceof ServiceAccountAuthCredentials, + "Signing key was not provided and could not be derived"); + cred = (ServiceAccountAuthCredentials) this.options().authCredentials(); + } + // construct signature data - see https://cloud.google.com/storage/docs/access-control#Signed-URLs + StringBuilder stBuilder = new StringBuilder(); + if (optionMap.containsKey(SignUrlOption.Option.HTTP_METHOD)) { + stBuilder.append(optionMap.get(SignUrlOption.Option.HTTP_METHOD)); + } else { + stBuilder.append(HttpMethod.GET); + } + stBuilder.append('\n'); + if (firstNonNull((Boolean) optionMap.get(SignUrlOption.Option.MD5) , false)) { + checkArgument(blobInfo.md5() != null, "Blob is missing a value for md5"); + stBuilder.append(blobInfo.md5()); + } + stBuilder.append('\n'); + if (firstNonNull((Boolean) optionMap.get(SignUrlOption.Option.CONTENT_TYPE) , false)) { + checkArgument(blobInfo.contentType() != null, "Blob is missing a value for content-type"); + stBuilder.append(blobInfo.contentType()); + } + stBuilder.append('\n'); + stBuilder.append(expiration).append('\n'); + StringBuilder path = new StringBuilder(); + if (!blobInfo.bucket().startsWith("/")) { + path.append('/'); + } + path.append(blobInfo.bucket()); + if (!blobInfo.bucket().endsWith("/")) { + path.append('/'); + } + if (blobInfo.name().startsWith("/")) { + path.setLength(stBuilder.length() - 1); + } + path.append(blobInfo.name()); + stBuilder.append(path); + try { + Signature signer = Signature.getInstance("SHA256withRSA"); + signer.initSign(cred.privateKey()); + signer.update(stBuilder.toString().getBytes(UTF_8)); + String signature = + URLEncoder.encode(BaseEncoding.base64().encode(signer.sign()), UTF_8.name()); + stBuilder = new StringBuilder("https://storage.googleapis.com").append(path); + stBuilder.append("?GoogleAccessId=").append(cred.account()); + stBuilder.append("&Expires=").append(expiration); + stBuilder.append("&Signature=").append(signature); + return new URL(stBuilder.toString()); + } catch (MalformedURLException | NoSuchAlgorithmException | UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } catch (SignatureException | InvalidKeyException e) { + throw new IllegalArgumentException("Invalid service account private key"); + } + } + + private Map optionMap(Long generation, Long metaGeneration, + Iterable options) { + return optionMap(generation, metaGeneration, options, false); + } + + private Map optionMap(Long generation, Long metaGeneration, + Iterable options, boolean useAsSource) { + Map temp = Maps.newEnumMap(StorageRpc.Option.class); + for (Option option : options) { + Object prev = temp.put(option.rpcOption(), option.value()); + checkArgument(prev == null, "Duplicate option %s", option); + } + Boolean value = (Boolean) temp.remove(DELIMITER); + if (Boolean.TRUE.equals(value)) { + temp.put(DELIMITER, options().pathDelimiter()); + } + if (useAsSource) { + addToOptionMap(IF_GENERATION_MATCH, IF_SOURCE_GENERATION_MATCH, generation, temp); + addToOptionMap(IF_GENERATION_NOT_MATCH, IF_SOURCE_GENERATION_NOT_MATCH, generation, temp); + addToOptionMap(IF_METAGENERATION_MATCH, IF_SOURCE_METAGENERATION_MATCH, metaGeneration, temp); + addToOptionMap(IF_METAGENERATION_NOT_MATCH, + IF_SOURCE_METAGENERATION_NOT_MATCH, metaGeneration, temp); + } else { + addToOptionMap(IF_GENERATION_MATCH, generation, temp); + addToOptionMap(IF_GENERATION_NOT_MATCH, generation, temp); + addToOptionMap(IF_METAGENERATION_MATCH, metaGeneration, temp); + addToOptionMap(IF_METAGENERATION_NOT_MATCH, metaGeneration, temp); + } + return ImmutableMap.copyOf(temp); + } + + private static void addToOptionMap(StorageRpc.Option option, T defaultValue, + Map map) { + addToOptionMap(option, option, defaultValue, map); + } + + private static void addToOptionMap(StorageRpc.Option getOption, StorageRpc.Option putOption, + T defaultValue, Map map) { + if (map.containsKey(getOption)) { + @SuppressWarnings("unchecked") + T value = (T) map.remove(getOption); + checkArgument(value != null || defaultValue != null, + "Option " + getOption.value() + " is missing a value"); + value = firstNonNull(value, defaultValue); + map.put(putOption, value); + } + } + + private Map optionMap(Option... options) { + return optionMap(null, null, Arrays.asList(options)); + } + + private Map optionMap(Long generation, Long metaGeneration, + Option... options) { + return optionMap(generation, metaGeneration, Arrays.asList(options)); + } + + private Map optionMap(BucketInfo bucketInfo, Option... options) { + return optionMap(null, bucketInfo.metageneration(), options); + } + + private Map optionMap(BlobInfo blobInfo, Option... options) { + return optionMap(blobInfo.generation(), blobInfo.metageneration(), options); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageOptions.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageOptions.java new file mode 100644 index 000000000000..9e4ba2b72407 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageOptions.java @@ -0,0 +1,117 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableSet; +import com.google.gcloud.ServiceOptions; +import com.google.gcloud.spi.DefaultStorageRpc; +import com.google.gcloud.spi.StorageRpc; +import com.google.gcloud.spi.StorageRpcFactory; + +import java.util.Objects; +import java.util.Set; + +public class StorageOptions extends ServiceOptions { + + private static final long serialVersionUID = -7804860602287801084L; + private static final String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control"; + private static final Set SCOPES = ImmutableSet.of(GCS_SCOPE); + private static final String DEFAULT_PATH_DELIMITER = "/"; + + private final String pathDelimiter; + private transient StorageRpc storageRpc; + + public static class Builder extends + ServiceOptions.Builder { + + private String pathDelimiter; + + private Builder() {} + + private Builder(StorageOptions options) { + super(options); + } + + public Builder pathDelimiter(String pathDelimiter) { + this.pathDelimiter = pathDelimiter; + return this; + } + + @Override + public StorageOptions build() { + return new StorageOptions(this); + } + } + + private StorageOptions(Builder builder) { + super(builder); + pathDelimiter = MoreObjects.firstNonNull(builder.pathDelimiter, DEFAULT_PATH_DELIMITER); + // todo: consider providing read-timeout + } + + @Override + protected Set scopes() { + return SCOPES; + } + + StorageRpc storageRpc() { + if (storageRpc != null) { + return storageRpc; + } + if (serviceRpcFactory() != null) { + storageRpc = serviceRpcFactory().create(this); + } else { + storageRpc = createRpc(this, StorageRpcFactory.class); + if (storageRpc == null) { + storageRpc = new DefaultStorageRpc(this); + } + } + return storageRpc; + } + + public String pathDelimiter() { + return pathDelimiter; + } + + @Override + public Builder toBuilder() { + return new Builder(this); + } + + @Override + public int hashCode() { + return super.hashCode() ^ Objects.hash(pathDelimiter); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StorageOptions)) { + return false; + } + StorageOptions other = (StorageOptions) obj; + return isEquals(other) && Objects.equals(pathDelimiter, other.pathDelimiter); + } + + public static StorageOptions defaultInstance() { + return builder().build(); + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/package-info.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/package-info.java new file mode 100644 index 000000000000..cb43dd0be1b2 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/package-info.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 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. + */ + +/** + * A client to Google Cloud Storage. + * + *

A simple usage example: + *

{@code
+ * StorageOptions options = StorageOptions.builder().projectId("project").build();
+ * Storage storage = StorageFactory.instance().get(options);
+ * byte[] content = readContent();
+ * BlobInfo blobInfo = storage.get("bucket", "blob_name");
+ * if (blobInfo == null) {
+ *   storage.create(BlobInfo.of("bucket", "blob_name"), content);
+ * } else {
+ *   byte[] prevContent = storage.readAllBytes("bucket", "blob_name");
+ *   content = mergeContent(prevContent, content);
+ *   WritableByteChannel channel = storage.writer(blob);
+ *   channel.write(ByteBuffer.wrap(content));
+ *   channel.close();
+ * }}
+ * + * @see Google Cloud Storage + */ +package com.google.gcloud.storage; + diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/AclTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/AclTest.java new file mode 100644 index 000000000000..6a11fb0b2810 --- /dev/null +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/AclTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static org.junit.Assert.assertEquals; + +import com.google.api.services.storage.model.BucketAccessControl; +import com.google.api.services.storage.model.ObjectAccessControl; +import com.google.gcloud.storage.Acl.Domain; +import com.google.gcloud.storage.Acl.Entity; +import com.google.gcloud.storage.Acl.Entity.Type; +import com.google.gcloud.storage.Acl.Group; +import com.google.gcloud.storage.Acl.Project; +import com.google.gcloud.storage.Acl.Project.ProjectRole; +import com.google.gcloud.storage.Acl.RawEntity; +import com.google.gcloud.storage.Acl.Role; +import com.google.gcloud.storage.Acl.User; + +import org.junit.Test; + +public class AclTest { + + @Test + public void testDomainEntity() { + Domain acl = new Domain("d1"); + assertEquals("d1", acl.domain()); + assertEquals(Type.DOMAIN, acl.type()); + String pb = acl.toPb(); + assertEquals(acl, Entity.fromPb(pb)); + } + + @Test + public void testGroupEntity() { + Group acl = new Group("g1"); + assertEquals("g1", acl.email()); + assertEquals(Type.GROUP, acl.type()); + String pb = acl.toPb(); + assertEquals(acl, Entity.fromPb(pb)); + } + + @Test + public void testUserEntity() { + User acl = new User("u1"); + assertEquals("u1", acl.email()); + assertEquals(Type.USER, acl.type()); + String pb = acl.toPb(); + assertEquals(acl, Entity.fromPb(pb)); + } + + @Test + public void testProjectEntity() { + Project acl = new Project(ProjectRole.VIEWERS, "p1"); + assertEquals(ProjectRole.VIEWERS, acl.projectRole()); + assertEquals("p1", acl.projectId()); + assertEquals(Type.PROJECT, acl.type()); + String pb = acl.toPb(); + assertEquals(acl, Entity.fromPb(pb)); + } + + @Test + public void testRawEntity() { + Entity acl = new RawEntity("bla"); + assertEquals("bla", acl.value()); + assertEquals(Type.UNKNOWN, acl.type()); + String pb = acl.toPb(); + assertEquals(acl, Entity.fromPb(pb)); + } + + + @Test + public void testAcl() { + Acl acl = new Acl(User.ofAllUsers(), Role.READER); + assertEquals(User.ofAllUsers(), acl.entity()); + assertEquals(Role.READER, acl.role()); + ObjectAccessControl objectPb = acl.toObjectPb(); + assertEquals(acl, Acl.fromPb(objectPb)); + BucketAccessControl bucketPb = acl.toBucketPb(); + assertEquals(acl, Acl.fromPb(bucketPb)); + } +} diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchRequestTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchRequestTest.java new file mode 100644 index 000000000000..9cafd64afaa8 --- /dev/null +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchRequestTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static com.google.gcloud.storage.Storage.PredefinedAcl.PUBLIC_READ; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.Iterables; +import com.google.gcloud.storage.Storage.BlobSourceOption; +import com.google.gcloud.storage.Storage.BlobTargetOption; + +import org.junit.Test; + +import java.util.Iterator; +import java.util.Map.Entry; + +public class BatchRequestTest { + + @Test + public void testBatchRequest() { + BatchRequest request = BatchRequest.builder() + .delete("b1", "o1") + .delete("b1", "o2", BlobSourceOption.generationMatch(1), + BlobSourceOption.metagenerationMatch(2)) + .update(BlobInfo.of("b2", "o1"), BlobTargetOption.predefinedAcl(PUBLIC_READ)) + .update(BlobInfo.of("b2", "o2")) + .get("b3", "o1") + .get("b3", "o2", BlobSourceOption.generationMatch(1)) + .get("b3", "o3") + .build(); + + Iterator>> deletes = request + .toDelete().entrySet().iterator(); + Entry> delete = deletes.next(); + assertEquals(BlobInfo.of("b1", "o1"), delete.getKey()); + assertTrue(Iterables.isEmpty(delete.getValue())); + delete = deletes.next(); + assertEquals(BlobInfo.of("b1", "o2"), delete.getKey()); + assertEquals(2, Iterables.size(delete.getValue())); + assertFalse(deletes.hasNext()); + + Iterator>> updates = request + .toUpdate().entrySet().iterator(); + Entry> update = updates.next(); + assertEquals(BlobInfo.of("b2", "o1"), update.getKey()); + assertEquals(1, Iterables.size(update.getValue())); + assertEquals(BlobTargetOption.predefinedAcl(PUBLIC_READ), + Iterables.getFirst(update.getValue(), null)); + update = updates.next(); + assertEquals(BlobInfo.of("b2", "o2"), update.getKey()); + assertTrue(Iterables.isEmpty(update.getValue())); + assertFalse(updates.hasNext()); + + Iterator>> gets = request + .toGet().entrySet().iterator(); + Entry> get = gets.next(); + assertEquals(BlobInfo.of("b3", "o1"), get.getKey()); + assertTrue(Iterables.isEmpty(get.getValue())); + get = gets.next(); + assertEquals(BlobInfo.of("b3", "o2"), get.getKey()); + assertEquals(1, Iterables.size(get.getValue())); + assertEquals(BlobSourceOption.generationMatch(1), + Iterables.getFirst(get.getValue(), null)); + get = gets.next(); + assertEquals(BlobInfo.of("b3", "o3"), get.getKey()); + assertTrue(Iterables.isEmpty(get.getValue())); + assertFalse(gets.hasNext()); + } +} diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchResponseTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchResponseTest.java new file mode 100644 index 000000000000..7e774a77c739 --- /dev/null +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BatchResponseTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static org.junit.Assert.assertEquals; + +import com.google.common.collect.ImmutableList; +import com.google.gcloud.storage.BatchResponse.Result; + +import org.junit.Test; + +import java.util.List; + +public class BatchResponseTest { + + private static final BlobInfo BLOB_INFO_1 = BlobInfo.of("b", "o1"); + private static final BlobInfo BLOB_INFO_2 = BlobInfo.of("b", "o2"); + private static final BlobInfo BLOB_INFO_3 = BlobInfo.of("b", "o3"); + + @Test + public void testBatchResponse() { + List> deletes = ImmutableList.of(Result.of(true), Result.of(false)); + List> updates = ImmutableList.of(Result.of(BLOB_INFO_1), Result.of(BLOB_INFO_2)); + List> gets = ImmutableList.of(Result.of(BLOB_INFO_2), Result.of(BLOB_INFO_3)); + BatchResponse response = new BatchResponse(deletes, updates, gets); + + assertEquals(deletes, response.deletes()); + assertEquals(updates, response.updates()); + assertEquals(gets, response.gets()); + } +} diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobInfoTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobInfoTest.java new file mode 100644 index 000000000000..018a59c9cfc4 --- /dev/null +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobInfoTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static com.google.gcloud.storage.Acl.Project.ProjectRole.VIEWERS; +import static com.google.gcloud.storage.Acl.Role.READER; +import static com.google.gcloud.storage.Acl.Role.WRITER; +import static org.junit.Assert.assertEquals; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gcloud.storage.Acl.Project; +import com.google.gcloud.storage.Acl.User; + +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +public class BlobInfoTest { + + private static final List ACL = ImmutableList.of( + new Acl(User.ofAllAuthenticatedUsers(), READER), + new Acl(new Project(VIEWERS, "p1"), WRITER)); + private static final Integer COMPONENT_COUNT = 2; + private static final String CONTENT_TYPE = "text/html"; + private static final String CACHE_CONTROL = "cache"; + private static final String CONTENT_DISPOSITION = "content-disposition"; + private static final String CONTENT_ENCODING = "UTF-8"; + private static final String CONTENT_LANGUAGE = "En"; + private static final String CRC32 = "0xFF00"; + private static final Long DELETE_TIME = System.currentTimeMillis(); + private static final String ETAG = "0xFF00"; + private static final Long GENERATION = 1L; + private static final String ID = "B/N:1"; + private static final String MD5 = "0xFF00"; + private static final String MEDIA_LINK = "http://media/b/n"; + private static final Map METADATA = ImmutableMap.of("n1", "v1", "n2", "v2"); + private static final Long META_GENERATION = 10L; + private static final User OWNER = new User("user@gmail.com"); + private static final String SELF_LINK = "http://storage/b/n"; + private static final Long SIZE = 1024L; + private static final Long UPDATE_TIME = DELETE_TIME - 1L; + private static final BlobInfo BLOB_INFO = BlobInfo.builder("b", "n") + .acl(ACL) + .componentCount(COMPONENT_COUNT) + .contentType(CONTENT_TYPE) + .cacheControl(CACHE_CONTROL) + .contentDisposition(CONTENT_DISPOSITION) + .contentEncoding(CONTENT_ENCODING) + .contentLanguage(CONTENT_LANGUAGE) + .crc32c(CRC32) + .deleteTime(DELETE_TIME) + .etag(ETAG) + .generation(GENERATION) + .id(ID) + .md5(MD5) + .mediaLink(MEDIA_LINK) + .metadata(METADATA) + .metageneration(META_GENERATION) + .owner(OWNER) + .selfLink(SELF_LINK) + .size(SIZE) + .updateTime(UPDATE_TIME) + .build(); + + @Test + public void testToBuilder() { + compareBlobs(BLOB_INFO, BLOB_INFO.toBuilder().build()); + BlobInfo blobInfo = BLOB_INFO.toBuilder().name("n2").bucket("b2").size(200L).build(); + assertEquals("n2", blobInfo.name()); + assertEquals("b2", blobInfo.bucket()); + assertEquals(Long.valueOf(200), blobInfo.size()); + blobInfo = blobInfo.toBuilder().name("n").bucket("b").size(SIZE).build(); + compareBlobs(BLOB_INFO, blobInfo); + } + + @Test + public void testOf() { + BlobInfo blobInfo = BlobInfo.of("b", "n"); + assertEquals("b", blobInfo.bucket()); + assertEquals("n", blobInfo.name()); + } + + @Test + public void testBuilder() { + assertEquals("b", BLOB_INFO.bucket()); + assertEquals("n", BLOB_INFO.name()); + assertEquals(ACL, BLOB_INFO.acl()); + assertEquals(COMPONENT_COUNT, BLOB_INFO.componentCount()); + assertEquals(CONTENT_TYPE, BLOB_INFO.contentType()); + assertEquals(CACHE_CONTROL, BLOB_INFO.cacheControl() ); + assertEquals(CONTENT_DISPOSITION, BLOB_INFO.contentDisposition()); + assertEquals(CONTENT_ENCODING, BLOB_INFO.contentEncoding()); + assertEquals(CONTENT_LANGUAGE, BLOB_INFO.contentLanguage()); + assertEquals(CRC32, BLOB_INFO.crc32c()); + assertEquals(DELETE_TIME, BLOB_INFO.deleteTime()); + assertEquals(ETAG, BLOB_INFO.etag()); + assertEquals(GENERATION, BLOB_INFO.generation()); + assertEquals(ID, BLOB_INFO.id()); + assertEquals(MD5, BLOB_INFO.md5()); + assertEquals(MEDIA_LINK, BLOB_INFO.mediaLink()); + assertEquals(METADATA, BLOB_INFO.metadata()); + assertEquals(META_GENERATION, BLOB_INFO.metageneration()); + assertEquals(OWNER, BLOB_INFO.owner()); + assertEquals(SELF_LINK, BLOB_INFO.selfLink()); + assertEquals(SIZE, BLOB_INFO.size()); + assertEquals(UPDATE_TIME, BLOB_INFO.updateTime()); + } + + private void compareBlobs(BlobInfo expected, BlobInfo value) { + assertEquals(expected, value); + assertEquals(expected.bucket(), value.bucket()); + assertEquals(expected.name(), value.name()); + assertEquals(expected.acl(), value.acl()); + assertEquals(expected.componentCount(), value.componentCount()); + assertEquals(expected.contentType(), value.contentType()); + assertEquals(expected.cacheControl(), value.cacheControl() ); + assertEquals(expected.contentDisposition(), value.contentDisposition()); + assertEquals(expected.contentEncoding(), value.contentEncoding()); + assertEquals(expected.contentLanguage(), value.contentLanguage()); + assertEquals(expected.crc32c(), value.crc32c()); + assertEquals(expected.deleteTime(), value.deleteTime()); + assertEquals(expected.etag(), value.etag()); + assertEquals(expected.generation(), value.generation()); + assertEquals(expected.id(), value.id()); + assertEquals(expected.md5(), value.md5()); + assertEquals(expected.mediaLink(), value.mediaLink()); + assertEquals(expected.metadata(), value.metadata()); + assertEquals(expected.metageneration(), value.metageneration()); + assertEquals(expected.owner(), value.owner()); + assertEquals(expected.selfLink(), value.selfLink()); + assertEquals(expected.size(), value.size()); + assertEquals(expected.updateTime(), value.updateTime()); + } + + @Test + public void testToPbAndFromPb() { + compareBlobs(BLOB_INFO, BlobInfo.fromPb(BLOB_INFO.toPb())); + } +} diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BucketInfoTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BucketInfoTest.java new file mode 100644 index 000000000000..27a5b4014f3f --- /dev/null +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BucketInfoTest.java @@ -0,0 +1,175 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static com.google.gcloud.storage.Acl.Project.ProjectRole.VIEWERS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.google.api.services.storage.model.Bucket.Lifecycle.Rule; +import com.google.common.collect.ImmutableList; +import com.google.gcloud.storage.Acl.Project; +import com.google.gcloud.storage.Acl.Role; +import com.google.gcloud.storage.Acl.User; +import com.google.gcloud.storage.BucketInfo.AgeDeleteRule; +import com.google.gcloud.storage.BucketInfo.CreatedBeforeDeleteRule; +import com.google.gcloud.storage.BucketInfo.DeleteRule; +import com.google.gcloud.storage.BucketInfo.DeleteRule.Type; +import com.google.gcloud.storage.BucketInfo.IsLiveDeleteRule; +import com.google.gcloud.storage.BucketInfo.Location; +import com.google.gcloud.storage.BucketInfo.NumNewerVersionsDeleteRule; +import com.google.gcloud.storage.BucketInfo.RawDeleteRule; +import com.google.gcloud.storage.BucketInfo.StorageClass; + +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +public class BucketInfoTest { + + private static final List ACL = ImmutableList.of( + new Acl(User.ofAllAuthenticatedUsers(), Role.READER), + new Acl(new Project(VIEWERS, "p1"), Role.WRITER)); + private static final String ETAG = "0xFF00"; + private static final String ID = "B/N:1"; + private static final Long META_GENERATION = 10L; + private static final User OWNER = new User("user@gmail.com"); + private static final String SELF_LINK = "http://storage/b/n"; + private static final Long CREATE_TIME = System.currentTimeMillis(); + private static final List CORS = Collections.singletonList(Cors.builder().build()); + private static final List DEFAULT_ACL = + Collections.singletonList(new Acl(User.ofAllAuthenticatedUsers(), Role.WRITER)); + private static final List DELETE_RULES = + Collections.singletonList(new AgeDeleteRule(5)); + private static final String INDEX_PAGE = "index.html"; + private static final String NOT_FOUND_PAGE = "error.html"; + private static final Location LOCATION = Location.asia(); + private static final StorageClass STORAGE_CLASS = StorageClass.standard(); + private static final Boolean VERSIONING_ENABLED = true; + private static final BucketInfo BUCKET_INFO = BucketInfo.builder("b") + .acl(ACL) + .etag(ETAG) + .id(ID) + .metageneration(META_GENERATION) + .owner(OWNER) + .selfLink(SELF_LINK) + .cors(CORS) + .createTime(CREATE_TIME) + .defaultAcl(DEFAULT_ACL) + .deleteRules(DELETE_RULES) + .indexPage(INDEX_PAGE) + .notFoundPage(NOT_FOUND_PAGE) + .location(LOCATION) + .storageClass(STORAGE_CLASS) + .versioningEnabled(VERSIONING_ENABLED) + .build(); + + @Test + public void testToBuilder() { + compareBuckets(BUCKET_INFO, BUCKET_INFO.toBuilder().build()); + BucketInfo bucketInfo = BUCKET_INFO.toBuilder().name("B").id("id").build(); + assertEquals("B", bucketInfo.name()); + assertEquals("id", bucketInfo.id()); + bucketInfo = bucketInfo.toBuilder().name("b").id(ID).build(); + compareBuckets(BUCKET_INFO, bucketInfo); + } + + @Test + public void testOf() { + BucketInfo bucketInfo = BucketInfo.of("bucket"); + assertEquals("bucket", bucketInfo.name()); + } + + @Test + public void testBuilder() { + assertEquals("b", BUCKET_INFO.name()); + assertEquals(ACL, BUCKET_INFO.acl()); + assertEquals(ETAG, BUCKET_INFO.etag()); + assertEquals(ID, BUCKET_INFO.id()); + assertEquals(META_GENERATION, BUCKET_INFO.metageneration()); + assertEquals(OWNER, BUCKET_INFO.owner()); + assertEquals(SELF_LINK, BUCKET_INFO.selfLink()); + assertEquals(CREATE_TIME, BUCKET_INFO.createTime()); + assertEquals(CORS, BUCKET_INFO.cors()); + assertEquals(DEFAULT_ACL, BUCKET_INFO.defaultAcl()); + assertEquals(DELETE_RULES, BUCKET_INFO.deleteRules()); + assertEquals(INDEX_PAGE, BUCKET_INFO.indexPage()); + assertEquals(NOT_FOUND_PAGE, BUCKET_INFO.notFoundPage()); + assertEquals(LOCATION, BUCKET_INFO.location()); + assertEquals(STORAGE_CLASS, BUCKET_INFO.storageClass()); + assertEquals(VERSIONING_ENABLED, BUCKET_INFO.versioningEnabled()); + } + + @Test + public void testToPbAndFromPb() { + compareBuckets(BUCKET_INFO, BucketInfo.fromPb(BUCKET_INFO.toPb())); + } + + private void compareBuckets(BucketInfo expected, BucketInfo value) { + assertEquals(expected, value); + assertEquals(expected.name(), value.name()); + assertEquals(expected.acl(), value.acl()); + assertEquals(expected.etag(), value.etag()); + assertEquals(expected.id(), value.id()); + assertEquals(expected.metageneration(), value.metageneration()); + assertEquals(expected.owner(), value.owner()); + assertEquals(expected.selfLink(), value.selfLink()); + assertEquals(expected.createTime(), value.createTime()); + assertEquals(expected.cors(), value.cors()); + assertEquals(expected.defaultAcl(), value.defaultAcl()); + assertEquals(expected.deleteRules(), value.deleteRules()); + assertEquals(expected.indexPage(), value.indexPage()); + assertEquals(expected.notFoundPage(), value.notFoundPage()); + assertEquals(expected.location(), value.location()); + assertEquals(expected.storageClass(), value.storageClass()); + assertEquals(expected.versioningEnabled(), value.versioningEnabled()); + } + + public void testLocation() { + assertEquals("ASIA", Location.asia().value()); + assertEquals("EN", Location.eu().value()); + assertEquals("US", Location.us().value()); + assertSame(Location.asia(), Location.of("asia")); + assertSame(Location.asia(), Location.of("EU")); + assertSame(Location.asia(), Location.of("uS")); + } + + public void testDeleteRules() { + AgeDeleteRule ageRule = new AgeDeleteRule(10); + assertEquals(10, ageRule.daysToLive()); + assertEquals(Type.AGE, ageRule.type()); + CreatedBeforeDeleteRule createBeforeRule = new CreatedBeforeDeleteRule(1); + assertEquals(10, createBeforeRule.timeMillis()); + assertEquals(Type.CREATE_BEFORE, createBeforeRule.type()); + NumNewerVersionsDeleteRule versionsRule = new NumNewerVersionsDeleteRule(2); + assertEquals(2, versionsRule.numNewerVersions()); + assertEquals(Type.NUM_NEWER_VERSIONS, versionsRule.type()); + IsLiveDeleteRule isLiveRule = new IsLiveDeleteRule(true); + assertTrue(isLiveRule.isLive()); + assertEquals(Type.IS_LIVE, isLiveRule.type()); + Rule rule = new Rule().set("a", "b"); + RawDeleteRule rawRule = new RawDeleteRule(rule); + assertEquals(Type.UNKNOWN, isLiveRule.type()); + ImmutableList rules = ImmutableList + .of(ageRule, createBeforeRule, versionsRule, isLiveRule, rawRule); + for (DeleteRule delRule : rules) { + assertEquals(delRule, DeleteRule.fromPb(delRule.toPb())); + } + } +} diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CorsTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CorsTest.java new file mode 100644 index 000000000000..f978cb87f3d1 --- /dev/null +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CorsTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static org.junit.Assert.assertEquals; + +import com.google.common.collect.ImmutableList; +import com.google.gcloud.storage.Cors.Origin; + +import org.junit.Test; + +import java.util.List; + +public class CorsTest { + + @Test + public void testOrigin() { + assertEquals("bla", Origin.of("bla").value()); + assertEquals("http://host:8080", Origin.of("http", "host", 8080).toString()); + assertEquals(Origin.of("*"), Origin.any()); + } + + @Test + public void corsTest() { + List origins = ImmutableList.of(Origin.any(), Origin.of("o")); + List headers = ImmutableList.of("h1", "h2"); + List methods = ImmutableList.of(HttpMethod.GET); + Cors cors = Cors.builder() + .maxAgeSeconds(100) + .origins(origins) + .responseHeaders(headers) + .methods(methods) + .build(); + + assertEquals(Integer.valueOf(100), cors.maxAgeSeconds()); + assertEquals(origins, cors.origins()); + assertEquals(methods, cors.methods()); + assertEquals(headers, cors.responseHeaders()); + } +} diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/ListResultTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/ListResultTest.java new file mode 100644 index 000000000000..8a2e69d0c084 --- /dev/null +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/ListResultTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static org.junit.Assert.assertEquals; + +import com.google.common.collect.ImmutableList; + +import org.junit.Test; + +import java.util.Collections; + +public class ListResultTest { + + @Test + public void testListResult() throws Exception { + ImmutableList values = ImmutableList.of("1", "2"); + final ListResult nextResult = + new ListResult<>(null, "c", Collections.emptyList()); + ListResult.NextPageFetcher fetcher = new ListResult.NextPageFetcher() { + + @Override + public ListResult nextPage() { + return nextResult; + } + }; + ListResult result = new ListResult(fetcher, "c", values); + assertEquals(nextResult, result.nextPage()); + assertEquals("c", result.nextPageCursor()); + assertEquals(values, ImmutableList.copyOf(result.iterator())); + + } +} diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/ObjectIdTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/ObjectIdTest.java new file mode 100644 index 000000000000..a3fd9eb3d850 --- /dev/null +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/ObjectIdTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class ObjectIdTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWithBucketAndObjectName() { + ObjectId id = new ObjectId("bucket", "path/to/object.txt"); + assertEquals("bucket", id.getBucketName()); + assertEquals("path/to/object.txt", id.getObjectName()); + assertEquals("gs://bucket/path/to/object.txt", id.toString()); + assertEquals(URI.create("gs://bucket/path/to/object.txt"), id.toURI()); + } + + @Test + public void createFromRelativeURI() { + ObjectId id = new ObjectId("path/to/object.txt"); + assertNull(id.getBucketName()); + assertEquals("path/to/object.txt", id.getObjectName()); + assertEquals("path/to/object.txt", id.toString()); + assertEquals(URI.create("path/to/object.txt"), id.toURI()); + } + + @Test + public void createFromAbsoluteURI() { + ObjectId id = new ObjectId("gs://bucket/path/to/object.txt"); + assertEquals("bucket", id.getBucketName()); + assertEquals("path/to/object.txt", id.getObjectName()); + assertEquals("gs://bucket/path/to/object.txt", id.toString()); + assertEquals(URI.create("gs://bucket/path/to/object.txt"), id.toURI()); + } + + @Test + public void nullBucketNameNotAllowed() { + thrown.expect(NullPointerException.class); + new ObjectId(null, "object"); + } + + @Test + public void emptyBucketNameNotAllowed() { + thrown.expect(IllegalArgumentException.class); + new ObjectId("", "object"); + } + + @Test + public void nullObjectNameNotAllowed() { + thrown.expect(NullPointerException.class); + new ObjectId("bucket", null); + } + + @Test + public void emptyObjectNameNotAllowed() { + thrown.expect(IllegalArgumentException.class); + new ObjectId("bucket", ""); + } + + @Test + public void slashObjectNameNotAllowed() { + thrown.expect(IllegalArgumentException.class); + new ObjectId("bucket", "/"); + } + + @Test + public void checkConsistentHashing() { + ObjectId id1 = new ObjectId("bucket", "object"); + ObjectId id2 = new ObjectId("gs://bucket/object"); + assertTrue(id1.equals(id2)); + assertTrue(id1.hashCode() == id2.hashCode()); + } +} diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/OptionTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/OptionTest.java new file mode 100644 index 000000000000..4665d04b2d82 --- /dev/null +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/OptionTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static org.junit.Assert.assertEquals; + +import com.google.gcloud.spi.StorageRpc; + +import org.junit.Test; + +public class OptionTest { + + @Test + public void testOption() { + Option option = new Option(StorageRpc.Option.DELIMITER, "/"); + assertEquals(StorageRpc.Option.DELIMITER, option.rpcOption()); + assertEquals("/", option.value()); + } + + @Test(expected=NullPointerException.class) + public void testIndexOutOfBoundsException() { + new Option(null, "/"); + } +} diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/SerializationTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/SerializationTest.java new file mode 100644 index 000000000000..e79163da3a7e --- /dev/null +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/SerializationTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2015 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.gcloud.storage; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + +import com.google.gcloud.AuthCredentials; +import com.google.gcloud.RetryParams; +import com.google.gcloud.storage.Acl.Project.ProjectRole; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Collections; + +public class SerializationTest { + + private static final Acl.Domain ACL_DOMAIN = new Acl.Domain("domain"); + private static final Acl.Group ACL_GROUP = new Acl.Group("group"); + private static final Acl.Project ACL_PROJECT_ = new Acl.Project(ProjectRole.VIEWERS, "pid"); + private static final Acl.User ACL_USER = new Acl.User("user"); + private static final Acl.RawEntity ACL_RAW = new Acl.RawEntity("raw"); + private static final BlobInfo BLOB_INFO = BlobInfo.of("b", "n"); + private static final BucketInfo BUCKET_INFO = BucketInfo.of("b"); + private static final Cors.Origin ORIGIN = Cors.Origin.any(); + private static final Cors CORS = + Cors.builder().maxAgeSeconds(1).origins(Collections.singleton(ORIGIN)).build(); + private static final BatchRequest BATCH_REQUEST = BatchRequest.builder().delete("B", "N").build(); + private static final BatchResponse BATCH_RESPONSE = new BatchResponse( + Collections.singletonList(BatchResponse.Result.of(true)), + Collections.>emptyList(), + Collections.>emptyList()); + private static final ListResult LIST_RESULT = + new ListResult<>(null, "c", Collections.singletonList(BlobInfo.of("b", "n"))); + private static Storage.BlobListOption BLOB_LIST_OPTIONS = + Storage.BlobListOption.maxResults(100); + private static Storage.BlobSourceOption BLOB_SOURCE_OPTIONS = + Storage.BlobSourceOption.generationMatch(1); + private static Storage.BlobTargetOption BLOB_TARGET_OPTIONS = + Storage.BlobTargetOption.generationMatch(); + private static Storage.BucketListOption BUCKET_LIST_OPTIONS = + Storage.BucketListOption.prefix("bla"); + private static Storage.BucketSourceOption BUCKET_SOURCE_OPTIONS = + Storage.BucketSourceOption.metagenerationMatch(1); + private static Storage.BucketTargetOption BUCKET_TARGET_OPTIONS = + Storage.BucketTargetOption.metagenerationNotMatch(); + + @Test + public void testServiceOptions() throws Exception { + StorageOptions options = StorageOptions.builder() + .projectId("p1") + .authCredentials(AuthCredentials.createForAppEngine()) + .build(); + StorageOptions serializedCopy = serializeAndDeserialize(options); + assertEquals(options, serializedCopy); + + options = options.toBuilder() + .projectId("p2") + .retryParams(RetryParams.getDefaultInstance()) + .authCredentials(AuthCredentials.noCredentials()) + .pathDelimiter(":") + .build(); + serializedCopy = serializeAndDeserialize(options); + assertEquals(options, serializedCopy); + } + + @Test + public void testModelAndRequests() throws Exception { + Serializable[] objects = {ACL_DOMAIN, ACL_GROUP, ACL_PROJECT_, ACL_USER, ACL_RAW, BLOB_INFO, + BUCKET_INFO, + ORIGIN, CORS, BATCH_REQUEST,BATCH_RESPONSE, LIST_RESULT, BLOB_LIST_OPTIONS, + BLOB_SOURCE_OPTIONS, BLOB_TARGET_OPTIONS, BUCKET_LIST_OPTIONS, BUCKET_SOURCE_OPTIONS, + BUCKET_TARGET_OPTIONS}; + for (Serializable obj : objects) { + Object copy = serializeAndDeserialize(obj); + assertEquals(obj, obj); + assertEquals(obj, copy); + assertNotSame(obj, copy); + assertEquals(copy, copy); + } + } + + @SuppressWarnings("unchecked") + private T serializeAndDeserialize(T obj) + throws IOException, ClassNotFoundException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ObjectOutputStream output = new ObjectOutputStream(bytes)) { + output.writeObject(obj); + } + try (ObjectInputStream input = + new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) { + return (T) input.readObject(); + } + } +} diff --git a/gcloud-java/README.md b/gcloud-java/README.md new file mode 100644 index 000000000000..2286627662fb --- /dev/null +++ b/gcloud-java/README.md @@ -0,0 +1,73 @@ +Google Cloud Java Client +========================== + +Java idiomatic client for [Google Cloud Platform][cloud-platform] services. + +[![Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-java.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/gcloud-java) +[![Coverage Status](https://coveralls.io/repos/GoogleCloudPlatform/gcloud-java/badge.svg?branch=master)](https://coveralls.io/r/GoogleCloudPlatform/gcloud-java?branch=master) + +- [Homepage] (https://googlecloudplatform.github.io/gcloud-java/) +- [API Documentation] (http://googlecloudplatform.github.io/gcloud-java/apidocs) +- [Examples] (http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/examples/package-summary.html) + +This client supports the following Google Cloud Platform services: + +- [Google Cloud Datastore] (https://cloud.google.com/datastore/) [datastore documentation][datastore-api] +- [Google Cloud Storage] (https://cloud.google.com/storage/) [storage documentation][storage-api] + +> Note: This client is a work-in-progress, and may occasionally +> make backwards-incompatible changes. + +Quickstart +---------- +Add this to your pom.xml file +```xml + + com.google.gcloud + gcloud-java + LATEST + +``` + +Contributing +------------ + +Contributions to this library are always welcome and highly encouraged. + +See [CONTRIBUTING] for more information on how to get started. + +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. + +License +------- + +Apache 2.0 - See [LICENSE] for more information. + + +[CONTRIBUTING]:https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/CONTRIBUTING.md +[LICENSE]: https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/LICENSE +[cloud-platform]: https://cloud.google.com/ +[cloud-datastore]: https://cloud.google.com/datastore/docs +[cloud-datastore-docs]: https://cloud.google.com/datastore/docs +[cloud-datastore-activation]: https://cloud.google.com/datastore/docs/activate +[datastore-api]: http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/datastore/package-summary.html + +[cloud-pubsub]: https://cloud.google.com/pubsub/ +[cloud-pubsub-docs]: https://cloud.google.com/pubsub/docs + +[cloud-storage]: https://cloud.google.com/storage/ +[cloud-storage-docs]: https://cloud.google.com/storage/docs/overview +[cloud-storage-create-bucket]: https://cloud.google.com/storage/docs/cloud-console#_creatingbuckets +[storage-api]: http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/storage/package-summary.html diff --git a/gcloud-java/pom.xml b/gcloud-java/pom.xml new file mode 100644 index 000000000000..fb1226a23d5e --- /dev/null +++ b/gcloud-java/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + com.google.gcloud + gcloud-java + jar + GCloud Java + https://github.com/GoogleCloudPlatform/gcloud-java + + Java idiomatic client for Google Cloud Platform services. + + + com.google.gcloud + gcloud-java-pom + 0.0.5 + + + + ${project.groupId} + gcloud-java-core + ${project.version} + + + ${project.groupId} + gcloud-java-datastore + ${project.version} + + + ${project.groupId} + gcloud-java-storage + ${project.version} + + + diff --git a/pmd.xml b/pmd.xml index 0b206c082bb3..62a87e6d063a 100644 --- a/pmd.xml +++ b/pmd.xml @@ -3,25 +3,20 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="pmd" xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd"> - PMD Plugin preferences rule set + PMD Plugin preferences rule set + .*/test/.* - - - - - - @@ -31,12 +26,8 @@ - - - - @@ -59,7 +50,6 @@ - @@ -67,7 +57,6 @@ - @@ -80,20 +69,12 @@ - - - - - - - - @@ -108,9 +89,13 @@ - + + + + + @@ -135,7 +120,6 @@ - @@ -150,10 +134,8 @@ - - @@ -172,26 +154,19 @@ - - - - - - - @@ -201,10 +176,7 @@ - - - @@ -224,9 +196,7 @@ - - @@ -245,10 +215,12 @@ - + + + + + - - @@ -258,7 +230,6 @@ - @@ -278,9 +249,6 @@ - - - @@ -314,7 +282,6 @@ - @@ -325,12 +292,9 @@ - - - - \ No newline at end of file + diff --git a/pom.xml b/pom.xml index 4a01c702f696..6705f19f6ee9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,92 +1,76 @@ - + 4.0.0 - com.ozarov.testing - git-demo - 0.0.1-SNAPSHOT - - - com.google.http-client - google-http-client - 1.19.0 - compile - - - com.google.oauth-client - google-oauth-client - 1.19.0 - compile - - - com.google.guava - guava - 18.0 - - - com.google.apis - google-api-services-datastore-protobuf - v1beta2-rev1-2.1.0 - compile - - - com.google.api-client - google-api-client-appengine - 1.19.0 - compile - - - guava-jdk5 - com.google.guava - - - - - junit - junit - 4.12 - test - - - joda-time - joda-time - RELEASE - compile - - - org.json - json - 20090211 - compile - - - com.google.apis - google-api-services-storage - v1-rev23-1.19.0 - compile - - - com.google.apis - google-api-services-datastore - v1beta2-rev23-1.19.0 - - - org.easymock - easymock - 3.3 - test - - - + com.google.gcloud + gcloud-java-pom + pom + 0.0.5 + GCloud Java + https://github.com/GoogleCloudPlatform/gcloud-java + + Java idiomatic client for Google Cloud Platform services. + + + + ozarov + Arie Ozarov + ozarov@google.com + Google + + Developer + + + + + Google + + + scm:git:git@github.com:GoogleCloudPlatform/gcloud-java.git + scm:git:git@github.com:GoogleCloudPlatform/gcloud-java.git + https://github.com/GoogleCloudPlatform/gcloud-java + HEAD + + + Travis CI + https://travis-ci.org/GoogleCloudPlatform/gcloud-java + + + https://github.com/GoogleCloudPlatform/gcloud-java/issues + GitHub Issues + + + + sonatype-nexus-snapshots + https://oss.sonatype.org/content/repositories/snapshots + - - false - - central - Central Repository - http://repo.maven.apache.org/maven2 + sonatype-nexus-staging + https://oss.sonatype.org/service/local/staging/deploy/maven2/ - + + github-pages-site + Deployment through GitHub's site deployment plugin + http://googlecloudplatform.github.io/gcloud-java/ + + + + + GCloud Java Software License + https://raw.githubusercontent.com/GoogleCloudPlatform/gcloud-java/master/LICENSE + + + + UTF-8 + UTF-8 + github + + + gcloud-java-core + gcloud-java-datastore + gcloud-java-storage + gcloud-java + gcloud-java-examples + @@ -101,10 +85,94 @@ + + + + org.codehaus.mojo + exec-maven-plugin + 1.3.2 + + true + java + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 1.4 + + + enforce-maven + + enforce + + + + + [3.0,) + + + [1.7,) + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.3.2 + + + + java + + + + + + -1 + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.18.1 + + + + integration-test + verify + + + + maven-jar-plugin 2.5 + + + true + true + + true + true + + + ${project.artifactId} + ${project.groupId} + ${project.version} + ${buildNumber} + + + maven-compiler-plugin @@ -113,7 +181,215 @@ 1.7 1.7 UTF-8 + -Xlint:unchecked + + + + org.apache.maven.plugins + maven-source-plugin + 2.4 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.1 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + deploy + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.3 + true + + sonatype-nexus-staging + https://oss.sonatype.org/ + true + + + + org.eluder.coveralls + coveralls-maven-plugin + 3.0.1 + + + ${basedir}/target/coverage.xml + + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.6 + + ${basedir}/target + + xml + html + + true + + true + + com/google/gcloud/**/*.class + + + com/google/gcloud/examples/**/*.class + + + 256m + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 2.13 + + + com.puppycrawl.tools + checkstyle + 6.2 + + + + + org.apache.maven.plugins + maven-site-plugin + 3.4 + + true + + + org.apache.maven.plugins + maven-changelog-plugin + 2.3 + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 2.8 + + + + index + dependency-info + dependencies + dependency-convergence + project-team + mailing-list + cim + issue-tracking + license + scm + dependency-management + distribution-management + summary + modules + + + + + true + true + true + true + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.10 + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.1 + + + html + + javadoc + + + + + true + protected + true + ${project.build.directory}/javadoc + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.18.1 + + + org.apache.maven.plugins + maven-checkstyle-plugin + 2.13 + + checkstyle.xml + false + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.6 + + true + + + + + + + com.github.github + site-maven-plugin + 0.10 + + Creating site for ${project.artifactId} ${project.version} + ${project.distributionManagement.site.url} + true + + + github-site + + site + + site-deploy + + diff --git a/src/main/java/com/google/gcloud/AuthConfig.java b/src/main/java/com/google/gcloud/AuthConfig.java deleted file mode 100644 index 67e7b572c133..000000000000 --- a/src/main/java/com/google/gcloud/AuthConfig.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.google.gcloud; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; -import com.google.api.client.googleapis.compute.ComputeCredential; -import com.google.api.client.googleapis.extensions.appengine.auth.oauth2.AppIdentityCredential; -import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; -import com.google.api.client.http.HttpRequestInitializer; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.jackson.JacksonFactory; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.PrivateKey; -import java.util.Set; - -public abstract class AuthConfig { - - private static class AppEngineAuthConfig extends AuthConfig { - - @Override - protected HttpRequestInitializer httpRequestInitializer( - HttpTransport transport, Set scopes) { - return new AppIdentityCredential(scopes); - } - } - - private static class ServiceAccountAuthConfig extends AuthConfig { - - private final String account; - private final PrivateKey privateKey; - - ServiceAccountAuthConfig(String account, PrivateKey privateKey) { - this.account = checkNotNull(account); - this.privateKey = checkNotNull(privateKey); - } - - ServiceAccountAuthConfig() { - account = null; - privateKey = null; - } - - @Override - protected HttpRequestInitializer httpRequestInitializer( - HttpTransport transport, Set scopes) { - GoogleCredential.Builder builder = new GoogleCredential.Builder() - .setTransport(transport) - .setJsonFactory(new JacksonFactory()); - if (privateKey != null) { - builder.setServiceAccountPrivateKey(privateKey); - builder.setServiceAccountId(account); - builder.setServiceAccountScopes(scopes); - } - return builder.build(); - } - } - - protected abstract HttpRequestInitializer httpRequestInitializer( - HttpTransport transport, Set scopes); - - - public static AuthConfig createForAppEngine() { - return new AppEngineAuthConfig(); - } - - public static AuthConfig createForComputeEngine() throws IOException, GeneralSecurityException { - final ComputeCredential cred = getComputeCredential(); - return new AuthConfig() { - @Override - protected HttpRequestInitializer httpRequestInitializer(HttpTransport ts, Set sc) { - return cred; - } - }; - } - - public static AuthConfig createFor(String account, PrivateKey privateKey) { - return new ServiceAccountAuthConfig(account, privateKey); - } - - public static AuthConfig noCredentials() { - return new ServiceAccountAuthConfig(); - } - - static ComputeCredential getComputeCredential() throws IOException, GeneralSecurityException { - NetHttpTransport transport = GoogleNetHttpTransport.newTrustedTransport(); - // Try to connect using Google Compute Engine service account credentials. - ComputeCredential credential = new ComputeCredential(transport, new JacksonFactory()); - // Force token refresh to detect if we are running on Google Compute Engine. - credential.refreshToken(); - return credential; - } -} diff --git a/src/main/java/com/google/gcloud/ServiceOptions.java b/src/main/java/com/google/gcloud/ServiceOptions.java deleted file mode 100644 index c23d754a5c46..000000000000 --- a/src/main/java/com/google/gcloud/ServiceOptions.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.google.gcloud; - - -import static com.google.common.base.MoreObjects.firstNonNull; - -import com.google.api.client.extensions.appengine.http.UrlFetchTransport; -import com.google.api.client.http.HttpRequestInitializer; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.javanet.NetHttpTransport; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -import java.net.URLConnection; -import java.util.Set; - -public abstract class ServiceOptions { - - private static final String DEFAULT_HOST = "https://www.googleapis.com"; - - private final String host; - private final HttpTransport httpTransport; - private final AuthConfig authConfig; - private final RetryParams retryParams; - - protected abstract static class Builder> { - - private String host; - private HttpTransport httpTransport; - private AuthConfig authConfig; - private RetryParams retryParams; - - protected Builder() {} - - protected Builder(ServiceOptions options) { - host = options.host; - httpTransport = options.httpTransport; - authConfig = options.authConfig; - retryParams = options.retryParams; - } - - protected abstract ServiceOptions build(); - - @SuppressWarnings("unchecked") - protected B self() { - return (B) this; - } - - public B host(String host) { - this.host = host; - return self(); - } - - public B httpTransport(HttpTransport httpTransport) { - this.httpTransport = httpTransport; - return self(); - } - - public B authConfig(AuthConfig authConfig) { - this.authConfig = authConfig; - return self(); - } - - public B retryParams(RetryParams retryParams) { - this.retryParams = retryParams; - return self(); - } - } - - protected ServiceOptions(Builder builder) { - host = firstNonNull(builder.host, DEFAULT_HOST); - httpTransport = firstNonNull(builder.httpTransport, defaultHttpTransport()); - authConfig = firstNonNull(builder.authConfig, defaultAuthConfig()); - retryParams = builder.retryParams; - } - - private static HttpTransport defaultHttpTransport() { - // Consider App Engine - if (appEngineAppId() != null) { - try { - return new UrlFetchTransport(); - } catch (Exception ignore) { - // Maybe not on App Engine - } - } - // Consider Compute - try { - return AuthConfig.getComputeCredential().getTransport(); - } catch (Exception e) { - // Maybe not on GCE - } - return new NetHttpTransport(); - } - - private static AuthConfig defaultAuthConfig() { - // Consider App Engine - if (appEngineAppId() != null) { - try { - return AuthConfig.createForAppEngine(); - } catch (Exception ignore) { - // Maybe not on App Engine - } - } - // Consider Compute - try { - return AuthConfig.createForComputeEngine(); - } catch (Exception ignore) { - // Maybe not on GCE - } - return AuthConfig.noCredentials(); - } - - protected static String appEngineAppId() { - return System.getProperty("com.google.appengine.application.id"); - } - - protected static String googleCloudProjectId() { - try { - URL url = new URL("http://metadata/computeMetadata/v1/project/project-id"); - URLConnection connection = url.openConnection(); - connection.setRequestProperty("X-Google-Metadata-Request", "True"); - try (BufferedReader reader = - new BufferedReader(new InputStreamReader(connection.getInputStream()))) { - return reader.readLine(); - } - } catch (IOException e) { - return null; - } - } - - protected abstract Set scopes(); - - public String host() { - return host; - } - - public HttpTransport httpTransport() { - return httpTransport; - } - - public AuthConfig authConfig() { - return authConfig; - } - - public RetryParams retryParams() { - return retryParams; - } - - protected HttpRequestInitializer httpRequestInitializer() { - return authConfig().httpRequestInitializer(httpTransport, scopes()); - } - - public abstract Builder toBuilder(); -} diff --git a/src/main/java/com/google/gcloud/datastore/BaseEntity.java b/src/main/java/com/google/gcloud/datastore/BaseEntity.java deleted file mode 100644 index 91665fd59987..000000000000 --- a/src/main/java/com/google/gcloud/datastore/BaseEntity.java +++ /dev/null @@ -1,218 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.gcloud.datastore.BlobValue.of; -import static com.google.gcloud.datastore.BooleanValue.of; -import static com.google.gcloud.datastore.DateTimeValue.of; -import static com.google.gcloud.datastore.DoubleValue.of; -import static com.google.gcloud.datastore.EntityValue.of; -import static com.google.gcloud.datastore.KeyValue.of; -import static com.google.gcloud.datastore.ListValue.of; -import static com.google.gcloud.datastore.LongValue.of; -import static com.google.gcloud.datastore.NullValue.of; -import static com.google.gcloud.datastore.StringValue.of; - -import com.google.api.services.datastore.DatastoreV1; -import com.google.common.collect.ImmutableSortedMap; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * A base class for entities to hold the properties. - */ -abstract class BaseEntity extends Serializable { - - private static final long serialVersionUID = 8175618724683792766L; - - private final transient ImmutableSortedMap> properties; - - protected abstract static class Builder> { - - protected final Map> properties; - - protected Builder() { - properties = new HashMap<>(); - } - - protected Builder(BaseEntity entity) { - properties = new HashMap<>(entity.properties()); - } - - @SuppressWarnings("unchecked") - protected B self() { - return (B) this; - } - - /** - * Clears all the properties. - */ - public B clear() { - properties.clear(); - return self(); - } - - /** - * Removes a property with the given {@code name}. - */ - public B remove(String name) { - properties.remove(name); - return self(); - } - - public B set(String name, Value value) { - properties.put(name, value); - return self(); - } - - public B set(String name, String value) { - properties.put(name, of(value)); - return self(); - } - - public B set(String name, long value) { - properties.put(name, of(value)); - return self(); - } - - public B set(String name, double value) { - properties.put(name, of(value)); - return self(); - } - - public B set(String name, boolean value) { - properties.put(name, of(value)); - return self(); - } - - public B set(String name, DateTime value) { - properties.put(name, of(value)); - return self(); - } - - public B set(String name, Key value) { - properties.put(name, of(value)); - return self(); - } - - public B set(String name, PartialEntity value) { - properties.put(name, of(value)); - return self(); - } - - public B set(String name, List> values) { - properties.put(name, of(values)); - return self(); - } - - public B set(String name, Value value, Value... other) { - properties.put(name, of(value, other)); - return self(); - } - - public B set(String name, Blob value) { - properties.put(name, of(value)); - return self(); - } - - public B setNull(String name) { - properties.put(name, of()); - return self(); - } - - public abstract BaseEntity build(); - } - - protected BaseEntity(ImmutableSortedMap> properties) { - this.properties = properties; - } - - /** - * Returns {@code true} if the entity contains a property with the given {@code name}. - */ - public boolean contains(String name) { - return properties.containsKey(name); - } - - /** - * Returns the {@link Value} for the given property {@code name}. - * - * @throws DatastoreServiceException if not such property. - */ - public > V getValue(String name) { - @SuppressWarnings("unchecked") - V property = (V) properties.get(name); - if (property == null) { - throw DatastoreServiceException.throwInvalidRequest("No such property %s", name); - } - return property; - } - - public boolean isNull(String name) { - return getValue(name) instanceof NullValue; - } - - public String getString(String name) { - return ((StringValue) getValue(name)).get(); - } - - public long getLong(String name) { - return ((LongValue) getValue(name)).get(); - } - - public double getDouble(String name) { - return ((DoubleValue) getValue(name)).get(); - } - - public boolean getBoolean(String name) { - return ((BooleanValue) getValue(name)).get(); - } - - public DateTime getDateTime(String name) { - return ((DateTimeValue) getValue(name)).get(); - } - - public Key getKey(String name) { - return ((KeyValue) getValue(name)).get(); - } - - @SuppressWarnings("unchecked") - public T getEntity(String name) { - return (T) ((EntityValue) getValue(name)).get(); - } - - public List> getList(String name) { - return ((ListValue) getValue(name)).get(); - } - - public Blob getBlob(String name) { - return ((BlobValue) getValue(name)).get(); - } - - /** - * Returns the properties name. - */ - public Set names() { - return properties.keySet(); - } - - ImmutableSortedMap> properties() { - return properties; - } - - @Override - protected final DatastoreV1.Entity toPb() { - DatastoreV1.Entity.Builder entityPb = DatastoreV1.Entity.newBuilder(); - for (Map.Entry> entry : properties.entrySet()) { - DatastoreV1.Property.Builder propertyPb = DatastoreV1.Property.newBuilder(); - propertyPb.setName(entry.getKey()); - propertyPb.setValue(entry.getValue().toPb()); - entityPb.addProperty(propertyPb.build()); - } - populateEntityBuilder(entityPb); - return entityPb.build(); - } - - protected abstract void populateEntityBuilder(DatastoreV1.Entity.Builder entityPb); -} diff --git a/src/main/java/com/google/gcloud/datastore/BatchWriteOption.java b/src/main/java/com/google/gcloud/datastore/BatchWriteOption.java deleted file mode 100644 index f2bf33266252..000000000000 --- a/src/main/java/com/google/gcloud/datastore/BatchWriteOption.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.google.gcloud.datastore; - -import com.google.common.collect.ImmutableMap; - -import java.util.Map; - -public abstract class BatchWriteOption implements java.io.Serializable { - - private static final long serialVersionUID = -3932758377282659839L; - - public static final class ForceWrites extends BatchWriteOption { - - private static final long serialVersionUID = 2555054296046232799L; - - private final boolean force; - - public ForceWrites(boolean force) { - this.force = force; - } - - public boolean force() { - return force; - } - } - - BatchWriteOption() { - // package protected - } - - public static ForceWrites forceWrites() { - return new ForceWrites(true); - } - - static Map, BatchWriteOption> asImmutableMap( - BatchWriteOption... options) { - ImmutableMap.Builder, BatchWriteOption> builder = - ImmutableMap.builder(); - for (BatchWriteOption option : options) { - builder.put(option.getClass(), option); - } - return builder.build(); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/BatchWriter.java b/src/main/java/com/google/gcloud/datastore/BatchWriter.java deleted file mode 100644 index 9fadd9eca6a9..000000000000 --- a/src/main/java/com/google/gcloud/datastore/BatchWriter.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.google.gcloud.datastore; - -/** - * An interface to represent a batch of write operations. - * Any write operation that is applied on a batch will only be sent - * to the Datastore upon {@link #submit} and with as few RPC calls as possible. - * A usage example: - *
 {@code
- *   Entity entity1 = datastore.get(key1);
- *   BatchWriter batchWriter = datastore.newBatchWriter();
- *   Entity entity2 = Entity.builder(key2).set("name", "John").build();
- *   entity1 = Entity.builder(entity1).clear().setNull("bla").build();
- *   Entity entity3 = Entity.builder(key3).set("title", new StringValue("title")).build();
- *   batchWriter.update(entity1);
- *   batchWriter.add(entity2, entity3);
- *   batchWriter.submit();
- * } 
- */ -public interface BatchWriter extends DatastoreWriter { - - /** - * {@inheritDoc} - * This operation will be converted to {@link #put} operation for entities that were already - * marked for deletion in this batch. - * @throws DatastoreServiceException if a given entity already added to this batch or if batch - * is no longer active - */ - @Override - void add(Entity... entity); - - /** - * {@inheritDoc} - * This operation will be converted to {@link #put} operation for entities that were already - * added or put in this batch. - * @throws DatastoreServiceException if an entity is marked for deletion in this batch or if - * batch is no longer active - */ - @Override - void update(Entity... entity); - - /** - * {@inheritDoc} - * This operation will also remove from this batch any prior writes for entities with the same - * keys. - * @throws DatastoreServiceException if batch is no longer active - */ - @Override - public void delete(Key... key); - - /** - * {@inheritDoc} - * This operation will also remove from this batch any prior writes for the same entities. - * @throws DatastoreServiceException if batch is no longer active - */ - @Override - public void put(Entity... entity); - - /** - * Submit the batch to the Datastore. - * - * @throws DatastoreServiceException if there was any failure or if batch is not longer active - */ - void submit(); - - /** - * Returns {@code true} if batch is still active (was not submitted). - */ - boolean active(); -} diff --git a/src/main/java/com/google/gcloud/datastore/BatchWriterImpl.java b/src/main/java/com/google/gcloud/datastore/BatchWriterImpl.java deleted file mode 100644 index 3589fe3fec24..000000000000 --- a/src/main/java/com/google/gcloud/datastore/BatchWriterImpl.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.gcloud.datastore.DatastoreServiceException.throwInvalidRequest; - -import com.google.api.services.datastore.DatastoreV1; -import com.google.gcloud.datastore.BatchWriteOption.ForceWrites; - -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; - -class BatchWriterImpl implements BatchWriter { - - private final LinkedHashMap toAdd = new LinkedHashMap<>(); - private final LinkedHashMap toUpdate = new LinkedHashMap<>(); - private final LinkedHashMap toPut = new LinkedHashMap<>(); - private final LinkedHashSet toDelete = new LinkedHashSet<>(); - private final boolean force; - protected final DatastoreServiceImpl datastore; - - private boolean active = true; - - BatchWriterImpl(DatastoreServiceImpl datastore, BatchWriteOption... options) { - this.datastore = datastore; - Map, BatchWriteOption> optionsMap = - BatchWriteOption.asImmutableMap(options); - if (optionsMap.containsKey(ForceWrites.class)) { - force = ((ForceWrites) optionsMap.get(ForceWrites.class)).force(); - } else { - force = datastore.options().force(); - } - } - - protected void checkActive() { - if (!active) { - throwInvalidRequest(getName() + " is no longer active"); - } - } - - protected String getName() { - return "batch"; - } - - @Override - public void add(Entity... entities) { - checkActive(); - for (Entity entity : entities) { - Key key = entity.key(); - if (toAdd.containsKey(key) || toUpdate.containsKey(key) || toPut.containsKey(key)) { - throw throwInvalidRequest("Entity with the key %s was already added or updated in this " - + getName(), entity.key()); - } - if (toDelete.remove(key)) { - toPut.put(key, entity); - } else { - toAdd.put(key, entity); - } - } - } - - @Override - public void update(Entity... entities) { - checkActive(); - for (Entity entity : entities) { - Key key = entity.key(); - if (toDelete.contains(key)) { - throw throwInvalidRequest( - "Entity with the key %s was already deleted in this " + getName(), entity.key()); - } - if (toAdd.remove(key) != null || toPut.containsKey(key)) { - toPut.put(key, entity); - } else { - toUpdate.put(key, entity); - } - } - } - - @Override - public void put(Entity... entities) { - checkActive(); - for (Entity entity : entities) { - Key key = entity.key(); - toAdd.remove(key); - toUpdate.remove(key); - toDelete.remove(key); - toPut.put(key, entity); - } - } - - @Override - public void delete(Key... keys) { - checkActive(); - for (Key key : keys) { - toAdd.remove(key); - toUpdate.remove(key); - toPut.remove(key); - toDelete.add(key); - } - } - - @Override - public void submit() { - checkActive(); - DatastoreV1.Mutation.Builder mutationPb = DatastoreV1.Mutation.newBuilder(); - for (Entity entity : toAdd.values()) { - mutationPb.addInsert(entity.toPb()); - } - for (Entity entity : toUpdate.values()) { - mutationPb.addUpdate(entity.toPb()); - } - for (Entity entity : toPut.values()) { - mutationPb.addUpsert(entity.toPb()); - } - for (Key key : toDelete) { - mutationPb.addDelete(key.toPb()); - } - if (force) { - mutationPb.setForce(force); - } - DatastoreV1.CommitRequest.Builder requestPb = newCommitRequest(); - requestPb.setMutation(mutationPb); - datastore.commit(requestPb.build()); - active = false; - } - - @Override - public boolean active() { - return active; - } - - protected DatastoreV1.CommitRequest.Builder newCommitRequest() { - DatastoreV1.CommitRequest.Builder requestPb = DatastoreV1.CommitRequest.newBuilder(); - requestPb.setMode(DatastoreV1.CommitRequest.Mode.NON_TRANSACTIONAL); - return requestPb; - } -} diff --git a/src/main/java/com/google/gcloud/datastore/BlobValue.java b/src/main/java/com/google/gcloud/datastore/BlobValue.java deleted file mode 100644 index 73671e167657..000000000000 --- a/src/main/java/com/google/gcloud/datastore/BlobValue.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.api.services.datastore.DatastoreV1.Value.BLOB_VALUE_FIELD_NUMBER; - -import com.google.api.services.datastore.DatastoreV1; - -public final class BlobValue extends Value { - - private static final long serialVersionUID = -5096238337676649540L; - - static final BaseMarshaller MARSHALLER = - new BaseMarshaller() { - - @Override - public int getProtoFieldId() { - return BLOB_VALUE_FIELD_NUMBER; - } - - @Override - public Builder newBuilder(Blob value) { - return builder(value); - } - - @Override - protected Blob getValue(DatastoreV1.Value from) { - return new Blob(from.getBlobValue(), false); - } - - @Override - protected void setValue(BlobValue from, DatastoreV1.Value.Builder to) { - to.setBlobValue(from.get().byteString()); - } - }; - - public static final class Builder extends Value.BaseBuilder { - - private Builder() { - super(Type.BLOB); - } - - @Override - public BlobValue build() { - return new BlobValue(this); - } - } - - public BlobValue(Blob blob) { - this(builder(blob)); - } - - private BlobValue(Builder builder) { - super(builder); - } - - @Override - public Builder toBuilder() { - return new Builder().mergeFrom(this); - } - - public static BlobValue of(Blob blob) { - return new BlobValue(blob); - } - - public static Builder builder(Blob blob) { - return new Builder().set(blob); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/BooleanValue.java b/src/main/java/com/google/gcloud/datastore/BooleanValue.java deleted file mode 100644 index 86e080b096fb..000000000000 --- a/src/main/java/com/google/gcloud/datastore/BooleanValue.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.api.services.datastore.DatastoreV1.Value.BOOLEAN_VALUE_FIELD_NUMBER; - -import com.google.api.services.datastore.DatastoreV1; - -public final class BooleanValue extends Value { - - private static final long serialVersionUID = -542649497897250340L; - - static final BaseMarshaller MARSHALLER = - new BaseMarshaller() { - - @Override - public int getProtoFieldId() { - return BOOLEAN_VALUE_FIELD_NUMBER; - } - - @Override - public Builder newBuilder(Boolean value) { - return builder(value); - } - - @Override - protected Boolean getValue(DatastoreV1.Value from) { - return from.getBooleanValue(); - } - - @Override - protected void setValue(BooleanValue from, DatastoreV1.Value.Builder to) { - to.setBooleanValue(from.get()); - } - }; - - public static final class Builder extends Value.BaseBuilder { - - private Builder() { - super(Type.BOOLEAN); - } - - @Override - public BooleanValue build() { - return new BooleanValue(this); - } - } - - public BooleanValue(boolean value) { - this(builder(value)); - } - - private BooleanValue(Builder builder) { - super(builder); - } - - @Override - public Builder toBuilder() { - return new Builder().mergeFrom(this); - } - - public static BooleanValue of(boolean value) { - return new BooleanValue(value); - } - - public static Builder builder(boolean value) { - return new Builder().set(value); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/DatastoreHelper.java b/src/main/java/com/google/gcloud/datastore/DatastoreHelper.java deleted file mode 100644 index 7fe4aeda0d96..000000000000 --- a/src/main/java/com/google/gcloud/datastore/DatastoreHelper.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.google.gcloud.datastore; - -import com.google.common.collect.Maps; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - * Adds some functionality to DatastoreService that should - * be provided statically to the interface (Java 8). - * - */ -public class DatastoreHelper implements DatastoreService { - - private final DatastoreService delegate; - - private DatastoreHelper(DatastoreService delegate) { - this.delegate = delegate; - } - - @Override - public Entity get(Key key) { - return delegate.get(key); - } - - @Override - public Iterator get(Key key, Key... others) { - return delegate.get(key, others); - } - - @Override - public QueryResult run(Query query) { - return delegate.run(query); - } - - @Override - public DatastoreServiceOptions options() { - return delegate.options(); - } - - @Override - public Transaction newTransaction(TransactionOption... options) { - return delegate.newTransaction(options); - } - - @Override - public BatchWriter newBatchWriter(BatchWriteOption... options) { - return delegate.newBatchWriter(options); - } - - @Override - public Key allocateId(PartialKey key) { - return delegate.allocateId(key); - } - - @Override - public Iterator allocateId(PartialKey key, PartialKey... others) { - return delegate.allocateId(key, others); - } - - @Override - public void add(Entity... entity) { - delegate.add(entity); - } - - @Override - public void update(Entity... entity) { - delegate.update(entity); - } - - @Override - public void put(Entity... entity) { - delegate.put(entity); - } - - @Override - public void delete(Key... key) { - delegate.delete(key); - } - - /** - * Returns a new KeyFactory for this service - */ - public KeyFactory newKeyFactory() { - return new KeyFactory(this); - } - - /** - * Returns a list with a value for each given key (ordered by input). - * A {@code null} would be returned for non-existing keys. - */ - public List fetch(Key key, Key... others) { - Iterator entities = delegate.get(key, others); - Map map = Maps.newHashMapWithExpectedSize(1 + others.length); - while (entities.hasNext()) { - Entity entity = entities.next(); - map.put(entity.key(), entity); - } - List list = new ArrayList<>(1 + others.length); - list.add(map.get(key)); - for (Key other : others) { - list.add(map.get(other)); - } - return list; - } - - public interface RunInTransaction { - void run(DatastoreReaderWriter readerWriter); - } - - public void runInTransaction(RunInTransaction runFor, TransactionOption... options) { - Transaction transaction = newTransaction(options); - try { - runFor.run(transaction); - transaction.commit(); - } finally { - if (transaction.active()) { - transaction.rollback(); - } - } - } - - public static DatastoreHelper createFor(DatastoreService datastoreService) { - if (datastoreService instanceof DatastoreHelper) { - return (DatastoreHelper) datastoreService; - } - return new DatastoreHelper(datastoreService); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/DatastoreReader.java b/src/main/java/com/google/gcloud/datastore/DatastoreReader.java deleted file mode 100644 index ea33c0104388..000000000000 --- a/src/main/java/com/google/gcloud/datastore/DatastoreReader.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.google.gcloud.datastore; - -import java.util.Iterator; - -/** - * An interface to represent Google Cloud Datastore read operations. - */ -public interface DatastoreReader { - - /** - * Returns an {@link Entity} for the given {@link Key} or {@code null} if does not exists. - * - * @throws DatastoreServiceException upon failure. - */ - Entity get(Key key); - - /** - * Returns an {@link Entity} for each given {@link Key} that exists in the Datastore. - * The order of the result is unspecified. - * Results are loaded lazily therefore it is possible to get a {@code DatastoreServiceException} - * from the returned {@code Iterator}'s {@link Iterator#hasNext hasNext} or - * {@link Iterator#next next} methods. - * - * @throws DatastoreServiceException upon failure. - */ - Iterator get(Key key, Key... others); - - /** - * Submit a {@link Query} and returns its result. - * - * @throws DatastoreServiceException upon failure. - */ - QueryResult run(Query query); -} diff --git a/src/main/java/com/google/gcloud/datastore/DatastoreReaderWriter.java b/src/main/java/com/google/gcloud/datastore/DatastoreReaderWriter.java deleted file mode 100644 index 52304f4e7e27..000000000000 --- a/src/main/java/com/google/gcloud/datastore/DatastoreReaderWriter.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.google.gcloud.datastore; - - -/** - * An interface that combines both Google Cloud Datastore read and write operations. - */ -public interface DatastoreReaderWriter extends DatastoreReader, DatastoreWriter { -} diff --git a/src/main/java/com/google/gcloud/datastore/DatastoreService.java b/src/main/java/com/google/gcloud/datastore/DatastoreService.java deleted file mode 100644 index f0f84191b18e..000000000000 --- a/src/main/java/com/google/gcloud/datastore/DatastoreService.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.google.gcloud.datastore; - -import java.util.Iterator; - -/** - * An interface for Google Cloud Datastore dataset. - */ -public interface DatastoreService extends DatastoreReaderWriter { - - /** - * Returns the {@code DatastoreServiceOptions} for this service. - */ - DatastoreServiceOptions options(); - - /** - * Returns a new Datastore transaction. - * - * @throws DatastoreServiceExcepiton upon failure - */ - Transaction newTransaction(TransactionOption... options); - - /** - * Returns a new Batch writer for processing multiple write operations - * in one request. - */ - BatchWriter newBatchWriter(BatchWriteOption... options); - - /** - * Allocate a unique id for the given key. - * The returned key will have the same information (dataset, kind, namespace and ancestors) - * as the given key and will have a newly assigned id. - * - * @throws DatastoreServiceExcepiton upon failure - */ - Key allocateId(PartialKey key); - - /** - * Returns a list of keys using the allocated ids ordered by the input. - * - * @see #allocateId(PartialKey) - * @throws DatastoreServiceExcepiton upon failure - */ - Iterator allocateId(PartialKey key, PartialKey... others); - - /** - * {@inheritDoc} - * @throws DatastoreServiceExcepiton upon failure - */ - @Override - void add(Entity... entity); - - /** - * {@inheritDoc} - * @throws DatastoreServiceExcepiton upon failure - */ - @Override - void update(Entity... entity); - - /** - * {@inheritDoc} - * @throws DatastoreServiceExcepiton upon failure - */ - @Override - void put(Entity... entity); - - /** - * {@inheritDoc} - * @throws DatastoreServiceExcepiton upon failure - */ - @Override - void delete(Key... key); -} diff --git a/src/main/java/com/google/gcloud/datastore/DatastoreServiceException.java b/src/main/java/com/google/gcloud/datastore/DatastoreServiceException.java deleted file mode 100644 index dfb3c56cd262..000000000000 --- a/src/main/java/com/google/gcloud/datastore/DatastoreServiceException.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.google.gcloud.datastore; - -import com.google.api.services.datastore.client.DatastoreException; -import com.google.common.base.MoreObjects; -import com.google.common.collect.ImmutableMap; -import com.google.gcloud.RetryHelper; -import com.google.gcloud.RetryHelper.RetryHelperException; - -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONTokener; - -import java.util.HashMap; -import java.util.Map; - -public class DatastoreServiceException extends RuntimeException { - - private static final long serialVersionUID = 8170357898917041899L; - private static final ImmutableMap REASON_TO_CODE; - private static final ImmutableMap HTTP_TO_CODE; - - private final Code code; - - /** - * An error code to represent the failure. - * - * @see Google Cloud Datastore error codes - */ - public enum Code { - - ABORTED(true, "Request aborted", 409), - DEADLINE_EXCEEDED(true, "Deadline exceeded", 403), - UNAVAILABLE(true, "Could not reach service", 503), - FAILED_PRECONDITION(false, "Invalid request", 412), - INVALID_ARGUMENT(false, "Request parameter has an invalid value", 400), - PERMISSION_DENIED(false, "Unauthorized request", 403), - UNAUTHORIZED(false, "Unauthorized", 401), - RESOURCE_EXHAUSTED(false, "Quota exceeded", 402), - INTERNAL(false, "Server returned an error", 500), - UNKNOWN(false, "Unknown failure", -1); - - private final boolean isTransient; - private final String defaultMessage; - private final int httpCode; - - Code(boolean isTransient, String msg, int httpCode) { - this.isTransient = isTransient; - defaultMessage = msg; - this.httpCode = httpCode; - } - - public Integer httpCode() { - return httpCode; - } - - /** - * Returns {@code true} if this exception is transient and the same request could be retried. - * For any retry it is highly recommended to apply an exponential backoff. - */ - public boolean isTransient() { - return isTransient; - } - - DatastoreServiceException translate(DatastoreException exception, String msg) { - return new DatastoreServiceException(this, msg, exception); - } - } - - static { - ImmutableMap.Builder builder = ImmutableMap.builder(); - Map httpCodes = new HashMap<>(); - for (Code code : Code.values()) { - builder.put(code.name(), code); - httpCodes.put(code.httpCode(), code); - } - REASON_TO_CODE = builder.build(); - HTTP_TO_CODE = ImmutableMap.copyOf(httpCodes); - } - - public DatastoreServiceException(Code code, String msg, Exception cause) { - super(MoreObjects.firstNonNull(msg, code.defaultMessage), cause); - this.code = code; - } - - public DatastoreServiceException(Code code, String msg) { - this(code, msg, null); - } - - /** - * Returns the code associated with this exception. - */ - public Code code() { - return code; - } - - static DatastoreServiceException translateAndThrow(RetryHelperException ex) { - if (ex.getCause() instanceof DatastoreException) { - return translateAndThrow((DatastoreException) ex.getCause()); - } - if (ex instanceof RetryHelper.RetryInterruptedException) { - RetryHelper.RetryInterruptedException.propagate(); - } - throw new DatastoreServiceException(Code.UNKNOWN, ex.getMessage(), ex); - } - - /** - * Translate DatastoreException to DatastoreServiceException based on their - * HTTP error codes. This method will always throw a new DatastoreServiceException. - * - * @throws DatastoreServiceException every time - */ - static DatastoreServiceException translateAndThrow(DatastoreException exception) { - String message = exception.getMessage(); - String reason = ""; - if (message != null) { - try { - JSONObject json = new JSONObject(new JSONTokener(exception.getMessage())); - JSONObject error = json.getJSONObject("error").getJSONArray("errors").getJSONObject(0); - reason = error.getString("reason"); - message = error.getString("message"); - } catch (JSONException ex) { - // ignore - will be converted to unknown - } - } - Code code = REASON_TO_CODE.get(reason); - if (code == null) { - code = MoreObjects.firstNonNull(HTTP_TO_CODE.get(exception.getCode()), Code.UNKNOWN); - } - throw code.translate(exception, message); - } - - - /** - * Throw a DatastoreServiceException with {@code FAILED_PRECONDITION} code and the {@code msg} - * in a nested exception. - * - * @throws DatastoreServiceException every time - */ - static DatastoreServiceException throwInvalidRequest(String msg, Object... params) { - throw new DatastoreServiceException(Code.FAILED_PRECONDITION, String.format(msg, params)); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/DatastoreServiceFactory.java b/src/main/java/com/google/gcloud/datastore/DatastoreServiceFactory.java deleted file mode 100644 index 9a8bb1dd82bc..000000000000 --- a/src/main/java/com/google/gcloud/datastore/DatastoreServiceFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.google.gcloud.datastore; - - - -public abstract class DatastoreServiceFactory { - - private static final DatastoreServiceFactory INSTANCE = new DatastoreServiceFactory() { - @Override - public DatastoreService get(DatastoreServiceOptions options) { - return new DatastoreServiceImpl(options, options.datastore()); - } - }; - - public static DatastoreService getDefault(DatastoreServiceOptions options) { - return INSTANCE.get(options); - } - - public abstract DatastoreService get(DatastoreServiceOptions options); -} diff --git a/src/main/java/com/google/gcloud/datastore/DatastoreServiceImpl.java b/src/main/java/com/google/gcloud/datastore/DatastoreServiceImpl.java deleted file mode 100644 index a887c03076df..000000000000 --- a/src/main/java/com/google/gcloud/datastore/DatastoreServiceImpl.java +++ /dev/null @@ -1,317 +0,0 @@ -package com.google.gcloud.datastore; - -import com.google.api.services.datastore.DatastoreV1; -import com.google.api.services.datastore.client.Datastore; -import com.google.api.services.datastore.client.DatastoreException; -import com.google.common.base.MoreObjects; -import com.google.common.collect.AbstractIterator; -import com.google.gcloud.ExceptionHandler; -import com.google.gcloud.RetryHelper; -import com.google.gcloud.RetryHelper.RetryHelperException; -import com.google.gcloud.RetryParams; -import com.google.protobuf.ByteString; - -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.concurrent.Callable; - - -final class DatastoreServiceImpl implements DatastoreService { - - static final Key[] EMPTY_KEY_ARRAY = {}; - static final PartialKey[] EMPTY_PARTIAL_KEY_ARRAY = {}; - private static final ExceptionHandler.Interceptor EXCEPTION_HANDLER_INTERCEPTOR = - new ExceptionHandler.Interceptor() { - - private static final long serialVersionUID = 6911242958397733203L; - - @Override - public RetryResult shouldRetry(Exception exception, RetryResult retryResult) { - return null; - } - - @Override - public RetryResult shouldRetry(Exception exception) { - if (exception instanceof DatastoreServiceException) { - boolean isTransient = ((DatastoreServiceException) exception).code().isTransient(); - return isTransient - ? ExceptionHandler.Interceptor.RetryResult.RETRY - : ExceptionHandler.Interceptor.RetryResult.ABORT; - } - return null; - } - }; - private static final ExceptionHandler EXCEPTION_HANDLER = ExceptionHandler.builder() - .abortOn(RuntimeException.class, DatastoreException.class) - .interceptor(EXCEPTION_HANDLER_INTERCEPTOR).build(); - - private final DatastoreServiceOptions options; - private final Datastore datastore; - private final RetryParams retryParams; - - DatastoreServiceImpl(DatastoreServiceOptions options, Datastore datastore) { - this.options = options; - this.datastore = datastore; - retryParams = MoreObjects.firstNonNull(options.retryParams(), RetryParams.noRetries()); - } - - @Override - public DatastoreServiceOptions options() { - return options; - } - - @Override - public BatchWriter newBatchWriter(BatchWriteOption... batchWriteOption) { - return new BatchWriterImpl(this, batchWriteOption); - } - - @Override - public Transaction newTransaction(TransactionOption... transactionOption) { - return new TransactionImpl(this, transactionOption); - } - - @Override - public QueryResult run(Query query) { - return run(null, query); - } - - QueryResult run(DatastoreV1.ReadOptions readOptionsPb, Query query) { - return new QueryResultImpl<>(this, readOptionsPb, query); - } - - DatastoreV1.RunQueryResponse runQuery(final DatastoreV1.RunQueryRequest requestPb) { - try { - return RetryHelper.runWithRetries(new Callable() { - @Override public DatastoreV1.RunQueryResponse call() throws DatastoreException { - return datastore.runQuery(requestPb); - } - }, retryParams, EXCEPTION_HANDLER); - } catch (RetryHelperException e) { - throw DatastoreServiceException.translateAndThrow(e); - } - } - - @Override - public Key allocateId(PartialKey key) { - return allocateId(key, EMPTY_PARTIAL_KEY_ARRAY).next(); - } - - @Override - public Iterator allocateId(PartialKey key, PartialKey... others) { - DatastoreV1.AllocateIdsRequest.Builder requestPb = DatastoreV1.AllocateIdsRequest.newBuilder(); - requestPb.addKey(trimNameOrId(key).toPb()); - for (PartialKey other : others) { - requestPb.addKey(trimNameOrId(other).toPb()); - } - // TODO(ozarov): will need to populate "force" after b/18594027 is fixed. - DatastoreV1.AllocateIdsResponse responsePb = allocateIds(requestPb.build()); - final Iterator keys = responsePb.getKeyList().iterator(); - return new AbstractIterator() { - @Override protected Key computeNext() { - if (keys.hasNext()) { - return Key.fromPb(keys.next()); - } - return endOfData(); - } - }; - } - - DatastoreV1.AllocateIdsResponse allocateIds(final DatastoreV1.AllocateIdsRequest requestPb) { - try { - return RetryHelper.runWithRetries(new Callable() { - @Override public DatastoreV1.AllocateIdsResponse call() throws DatastoreException { - return datastore.allocateIds(requestPb); - } - }, retryParams, EXCEPTION_HANDLER); - } catch (RetryHelperException e) { - throw DatastoreServiceException.translateAndThrow(e); - } - } - - private PartialKey trimNameOrId(PartialKey key) { - if (key instanceof Key) { - return PartialKey.builder(key).build(); - } - return key; - } - - @Override - public Entity get(Key key) { - Iterator iter = get(key, EMPTY_KEY_ARRAY); - return iter.hasNext() ? iter.next() : null; - } - - @Override - public Iterator get(Key key, Key... others) { - return get(null, key, others); - } - - Iterator get(DatastoreV1.ReadOptions readOptionsPb, final Key key, final Key... others) { - DatastoreV1.LookupRequest.Builder requestPb = DatastoreV1.LookupRequest.newBuilder(); - if (readOptionsPb != null) { - requestPb.setReadOptions(readOptionsPb); - } - LinkedHashSet dedupKeys = new LinkedHashSet<>(); - dedupKeys.add(key); - dedupKeys.addAll(Arrays.asList(others)); - for (Key k : dedupKeys) { - requestPb.addKey(k.toPb()); - } - return new ResultsIterator(requestPb); - } - - final class ResultsIterator extends AbstractIterator { - - private final DatastoreV1.LookupRequest.Builder requestPb; - Iterator iter; - - ResultsIterator(DatastoreV1.LookupRequest.Builder requestPb) { - this.requestPb = requestPb; - loadResults(); - } - - private void loadResults() { - DatastoreV1.LookupResponse responsePb = lookup(requestPb.build()); - iter = responsePb.getFoundList().iterator(); - requestPb.clearKey(); - if (responsePb.getDeferredCount() > 0) { - requestPb.addAllKey(responsePb.getDeferredList()); - } - } - - @Override - protected Entity computeNext() { - if (iter.hasNext()) { - return Entity.fromPb(iter.next().getEntity()); - } - while (!iter.hasNext()) { - if (requestPb.getKeyCount() == 0) { - return endOfData(); - } - loadResults(); - } - return Entity.fromPb(iter.next().getEntity()); - } - } - - DatastoreV1.LookupResponse lookup(final DatastoreV1.LookupRequest requestPb) { - try { - return RetryHelper.runWithRetries(new Callable() { - @Override public DatastoreV1.LookupResponse call() throws DatastoreException { - return datastore.lookup(requestPb); - } - }, retryParams, EXCEPTION_HANDLER); - } catch (RetryHelperException e) { - throw DatastoreServiceException.translateAndThrow(e); - } - } - - @Override - public void add(Entity... entities) { - DatastoreV1.Mutation.Builder mutationPb = DatastoreV1.Mutation.newBuilder(); - LinkedHashSet keys = new LinkedHashSet<>(); - for (Entity entity : entities) { - if (!keys.add(entity.key())) { - throw DatastoreServiceException.throwInvalidRequest( - "Duplicate entity with the key %s", entity.key()); - } - mutationPb.addInsert(entity.toPb()); - } - commitMutation(mutationPb); - } - - @Override - public void update(Entity... entities) { - DatastoreV1.Mutation.Builder mutationPb = DatastoreV1.Mutation.newBuilder(); - LinkedHashMap dedupEntities = new LinkedHashMap<>(); - for (Entity entity : entities) { - dedupEntities.put(entity.key(), entity); - } - for (Entity entity : dedupEntities.values()) { - mutationPb.addUpdate(entity.toPb()); - } - commitMutation(mutationPb); - } - - @Override - public void put(Entity... entities) { - DatastoreV1.Mutation.Builder mutationPb = DatastoreV1.Mutation.newBuilder(); - LinkedHashMap dedupEntities = new LinkedHashMap<>(); - for (Entity entity : entities) { - dedupEntities.put(entity.key(), entity); - } - for (Entity e : dedupEntities.values()) { - mutationPb.addUpsert(e.toPb()); - } - commitMutation(mutationPb); - } - - @Override - public void delete(Key... keys) { - DatastoreV1.Mutation.Builder mutationPb = DatastoreV1.Mutation.newBuilder(); - LinkedHashSet dedupKeys = new LinkedHashSet<>(Arrays.asList(keys)); - for (Key key : dedupKeys) { - mutationPb.addDelete(key.toPb()); - } - commitMutation(mutationPb); - } - - private void commitMutation(DatastoreV1.Mutation.Builder mutationPb) { - if (options.force()) { - mutationPb.setForce(true); - } - DatastoreV1.CommitRequest.Builder requestPb = DatastoreV1.CommitRequest.newBuilder(); - requestPb.setMode(DatastoreV1.CommitRequest.Mode.NON_TRANSACTIONAL); - requestPb.setMutation(mutationPb); - commit(requestPb.build()); - } - - DatastoreV1.CommitResponse commit(final DatastoreV1.CommitRequest requestPb) { - try { - return RetryHelper.runWithRetries(new Callable() { - @Override public DatastoreV1.CommitResponse call() throws DatastoreException { - return datastore.commit(requestPb); - } - }, retryParams, EXCEPTION_HANDLER); - } catch (RetryHelperException e) { - throw DatastoreServiceException.translateAndThrow(e); - } - } - - ByteString requestTransactionId(DatastoreV1.BeginTransactionRequest.Builder requestPb) { - return beginTransaction(requestPb.build()).getTransaction(); - } - - DatastoreV1.BeginTransactionResponse beginTransaction( - final DatastoreV1.BeginTransactionRequest requestPb) { - try { - return RetryHelper.runWithRetries(new Callable() { - @Override public DatastoreV1.BeginTransactionResponse call() throws DatastoreException { - return datastore.beginTransaction(requestPb); - } - }, retryParams, EXCEPTION_HANDLER); - } catch (RetryHelperException e) { - throw DatastoreServiceException.translateAndThrow(e); - } - } - - void rollbackTransaction(ByteString transaction) { - DatastoreV1.RollbackRequest.Builder requestPb = DatastoreV1.RollbackRequest.newBuilder(); - requestPb.setTransaction(transaction); - rollback(requestPb.build()); - } - - DatastoreV1.RollbackResponse rollback(final DatastoreV1.RollbackRequest requestPb) { - try { - return RetryHelper.runWithRetries(new Callable() { - @Override public DatastoreV1.RollbackResponse call() throws DatastoreException { - return datastore.rollback(requestPb); - } - }, retryParams, EXCEPTION_HANDLER); - } catch (RetryHelperException e) { - throw DatastoreServiceException.translateAndThrow(e); - } - } -} diff --git a/src/main/java/com/google/gcloud/datastore/DatastoreServiceOptions.java b/src/main/java/com/google/gcloud/datastore/DatastoreServiceOptions.java deleted file mode 100644 index f3cc48d69801..000000000000 --- a/src/main/java/com/google/gcloud/datastore/DatastoreServiceOptions.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.common.base.MoreObjects.firstNonNull; -import static com.google.gcloud.datastore.Validator.validateDataset; -import static com.google.gcloud.datastore.Validator.validateNamespace; - -import com.google.api.client.http.HttpRequestInitializer; -import com.google.api.services.datastore.DatastoreV1; -import com.google.api.services.datastore.DatastoreV1.EntityResult; -import com.google.api.services.datastore.DatastoreV1.LookupResponse; -import com.google.api.services.datastore.client.Datastore; -import com.google.api.services.datastore.client.DatastoreException; -import com.google.api.services.datastore.client.DatastoreFactory; -import com.google.api.services.datastore.client.DatastoreOptions; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.google.gcloud.ServiceOptions; - -import java.lang.reflect.Method; -import java.util.Iterator; -import java.util.Set; - -public class DatastoreServiceOptions extends ServiceOptions { - - private static final String DATASET_ENV_NAME = "DATASTORE_DATASET"; - private static final String DATASTORE_SCOPE = "https://www.googleapis.com/auth/datastore"; - private static final String USERINFO_SCOPE = "https://www.googleapis.com/auth/userinfo.email"; - private static final Set SCOPES = ImmutableSet.of(DATASTORE_SCOPE, USERINFO_SCOPE); - private final String dataset; - private final String namespace; - private final boolean force; - private final Datastore datastore; - - public static class Builder extends ServiceOptions.Builder { - - private String dataset; - private String namespace; - private boolean force = false; - private Datastore datastore; - - private Builder() { - } - - private Builder(DatastoreServiceOptions options) { - super(options); - dataset = options.dataset; - force = options.force; - } - - @Override - public DatastoreServiceOptions build() { - return new DatastoreServiceOptions(this); - } - - public Builder datastore(Datastore datastore) { - this.datastore = datastore; - return this; - } - - public Builder dataset(String dataset) { - this.dataset = validateDataset(dataset); - return this; - } - - public Builder namespace(String namespace) { - this.namespace = validateNamespace(namespace); - return this; - } - - public Builder force(boolean force) { - this.force = force; - return this; - } - } - - DatastoreServiceOptions(Builder builder) { - super(builder); - namespace = builder.namespace != null ? builder.namespace : defaultNamespace(); - force = builder.force; - - // Replace provided dataset with full dataset (s~xxx, e~xxx,...) - String tempDataset = firstNonNull(builder.dataset, defaultDataset()); - Datastore tempDatastore = firstNonNull(builder.datastore, - defaultDatastore(tempDataset, host(), httpRequestInitializer())); - DatastoreV1.LookupRequest.Builder requestPb = DatastoreV1.LookupRequest.newBuilder(); - DatastoreV1.Key key = DatastoreV1.Key.newBuilder() - .addPathElement(DatastoreV1.Key.PathElement.newBuilder().setKind("__foo__").setName("bar")) - .build(); - requestPb.addKey(key); - try { - LookupResponse responsePb = tempDatastore.lookup(requestPb.build()); - if (responsePb.getDeferredCount() > 0) { - key = responsePb.getDeferred(0); - } else { - Iterator combinedIter = - Iterables.concat(responsePb.getMissingList(), responsePb.getFoundList()).iterator(); - key = combinedIter.next().getEntity().getKey(); - } - dataset = key.getPartitionId().getDatasetId(); - if (builder.datastore == null && !dataset.equals(tempDataset)) { - datastore = defaultDatastore(dataset, host(), httpRequestInitializer()); - } else { - datastore = tempDatastore; - } - } catch (DatastoreException e) { - throw DatastoreServiceException.translateAndThrow(e); - } - } - - private static Datastore defaultDatastore( - String dataset, String host, HttpRequestInitializer initializer) { - DatastoreOptions options = new DatastoreOptions.Builder() - .dataset(dataset) - .host(host) - .initializer(initializer) - .build(); - return DatastoreFactory.get().create(options); - } - - private static String defaultDataset() { - String dataset = System.getProperty(DATASET_ENV_NAME, System.getenv(DATASET_ENV_NAME)); - if (dataset == null) { - dataset = appEngineAppId(); - } - return dataset != null ? dataset : googleCloudProjectId(); - } - - public String dataset() { - return dataset; - } - - public String namespace() { - return namespace; - } - - private static String defaultNamespace() { - // TODO(ozarov): An alternative to reflection would be to depend on AE api jar: - // http://mvnrepository.com/artifact/com.google.appengine/appengine-api-1.0-sdk/1.2.0 - try { - Class clazz = Class.forName("com.google.appengine.api.NamespaceManager"); - Method method = clazz.getMethod("get"); - String namespace = (String) method.invoke(null); - return "".equals(namespace) ? null : namespace; - } catch (Exception ex) { - return null; - } - } - - public boolean force() { - return force; - } - - @Override - protected Set scopes() { - return SCOPES; - } - - @Override - public Builder toBuilder() { - return new Builder(this); - } - - public Datastore datastore() { - return datastore; - } - - public static Builder builder() { - return new Builder(); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/DatastoreWriter.java b/src/main/java/com/google/gcloud/datastore/DatastoreWriter.java deleted file mode 100644 index 5c87a49424ba..000000000000 --- a/src/main/java/com/google/gcloud/datastore/DatastoreWriter.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.google.gcloud.datastore; - -/** - * An interface to represent Google Cloud Datastore write operations. - */ -public interface DatastoreWriter { - - /** - * A Datastore add operation. - * The operation will fail if an entity with the same key already exists. - */ - void add(Entity... entity); - - /** - * A Datastore update operation. - * The operation will fail if an entity with the same key does not already exist. - */ - void update(Entity... entity); - - /** - * A Datastore put (a.k.a upsert) operation. - * The operation will add or modify the entities. - */ - void put(Entity... entity); - - /** - * A datastore delete operation. - * It is OK request a deletion of a non-existing entity. - */ - void delete(Key... key); -} diff --git a/src/main/java/com/google/gcloud/datastore/DateTimeValue.java b/src/main/java/com/google/gcloud/datastore/DateTimeValue.java deleted file mode 100644 index fff1b35d7f29..000000000000 --- a/src/main/java/com/google/gcloud/datastore/DateTimeValue.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.api.services.datastore.DatastoreV1.Value.TIMESTAMP_MICROSECONDS_VALUE_FIELD_NUMBER; - -import com.google.api.services.datastore.DatastoreV1; - -public final class DateTimeValue extends Value { - - private static final long serialVersionUID = -5096238337676649540L; - - static final BaseMarshaller MARSHALLER = - new BaseMarshaller() { - - @Override - public int getProtoFieldId() { - return TIMESTAMP_MICROSECONDS_VALUE_FIELD_NUMBER; - } - - @Override - public Builder newBuilder(DateTime value) { - return builder(value); - } - - @Override - protected DateTime getValue(DatastoreV1.Value from) { - return new DateTime(from.getTimestampMicrosecondsValue()); - } - - @Override - protected void setValue(DateTimeValue from, DatastoreV1.Value.Builder to) { - to.setTimestampMicrosecondsValue(from.get().timestampMicroseconds()); - } - }; - - public static final class Builder - extends Value.BaseBuilder { - - private Builder() { - super(Type.DATE_TIME); - } - - @Override - public DateTimeValue build() { - return new DateTimeValue(this); - } - } - - public DateTimeValue(DateTime dateTime) { - this(builder(dateTime)); - } - - private DateTimeValue(Builder builder) { - super(builder); - } - - @Override - public Builder toBuilder() { - return new Builder().mergeFrom(this); - } - - public static DateTimeValue of(DateTime dateTime) { - return new DateTimeValue(dateTime); - } - - public static Builder builder(DateTime dateTime) { - return new Builder().set(dateTime); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/DoubleValue.java b/src/main/java/com/google/gcloud/datastore/DoubleValue.java deleted file mode 100644 index 5d5b510739f0..000000000000 --- a/src/main/java/com/google/gcloud/datastore/DoubleValue.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.api.services.datastore.DatastoreV1.Value.DOUBLE_VALUE_FIELD_NUMBER; - -import com.google.api.services.datastore.DatastoreV1; - -public final class DoubleValue extends Value { - - private static final long serialVersionUID = -5096238337676649540L; - - static final BaseMarshaller MARSHALLER = - new BaseMarshaller() { - - @Override - public int getProtoFieldId() { - return DOUBLE_VALUE_FIELD_NUMBER; - } - - @Override - public Builder newBuilder(Double value) { - return builder(value); - } - - @Override - protected Double getValue(DatastoreV1.Value from) { - return from.getDoubleValue(); - } - - @Override - protected void setValue(DoubleValue from, DatastoreV1.Value.Builder to) { - to.setDoubleValue(from.get()); - } - }; - - public static final class Builder extends Value.BaseBuilder { - - public Builder() { - super(Type.DOUBLE); - } - - @Override - public DoubleValue build() { - return new DoubleValue(this); - } - } - - public DoubleValue(double value) { - this(builder(value)); - } - - private DoubleValue(Builder builder) { - super(builder); - } - - @Override - public Builder toBuilder() { - return new Builder().mergeFrom(this); - } - - public static DoubleValue of(double value) { - return new DoubleValue(value); - } - - public static Builder builder(double value) { - return new Builder().set(value); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/Entity.java b/src/main/java/com/google/gcloud/datastore/Entity.java deleted file mode 100644 index 2d4ae4306942..000000000000 --- a/src/main/java/com/google/gcloud/datastore/Entity.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.api.services.datastore.DatastoreV1; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableSortedMap; -import com.google.protobuf.InvalidProtocolBufferException; - -/** - * An entity is the Google Cloud Datastore persistent data object. - * An entity holds one or more properties, represented by a name (as {@link String}) - * and a value (as {@link Value}), and is associated with a {@link Key}. - * For a list of possible values see {@link Value.Type}. - * This class is immutable. - * - * @see Google Cloud Datastore Entities, Properties, and Keys - */ -public final class Entity extends PartialEntity { - - private static final long serialVersionUID = 432961565733066915L; - - public static final class Builder extends BaseEntity.Builder { - - private Key key; - - private Builder(Key key) { - this.key = checkNotNull(key); - } - - private Builder(Entity entity) { - super(entity); - key = entity.key(); - } - - public Builder key(Key key) { - this.key = checkNotNull(key); - return this; - } - - @Override - public Entity build() { - return new Entity(key, ImmutableSortedMap.copyOf(properties)); - } - } - - Entity(Key key, ImmutableSortedMap> properties) { - super(key, properties); - } - - /** - * Returns the entity's key (never null). - */ - @Override - public Key key() { - return (Key) super.key(); - } - - @Override - protected Object fromPb(byte[] bytesPb) throws InvalidProtocolBufferException { - return fromPb(DatastoreV1.Entity.parseFrom(bytesPb)); - } - - static Entity fromPb(DatastoreV1.Entity entityPb) { - PartialEntity entity = PartialEntity.fromPb(entityPb); - Preconditions.checkState(entity instanceof Entity, "Entity is not complete"); - return (Entity) entity; - } - - public static Builder builder(Key key) { - return new Builder(key); - } - - public static Builder builder(Entity copyFrom) { - return new Builder(copyFrom); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/EntityValue.java b/src/main/java/com/google/gcloud/datastore/EntityValue.java deleted file mode 100644 index 061cf6c57fa5..000000000000 --- a/src/main/java/com/google/gcloud/datastore/EntityValue.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.api.services.datastore.DatastoreV1.Value.ENTITY_VALUE_FIELD_NUMBER; - -import com.google.api.services.datastore.DatastoreV1; -import com.google.common.base.Preconditions; - -public class EntityValue extends Value { - - private static final long serialVersionUID = -5461475706792576395L; - - static final BaseMarshaller MARSHALLER = - new BaseMarshaller() { - - @Override - public int getProtoFieldId() { - return ENTITY_VALUE_FIELD_NUMBER; - } - - @Override - public Builder newBuilder(PartialEntity value) { - return builder(value); - } - - @Override - protected PartialEntity getValue(DatastoreV1.Value from) { - return PartialEntity.fromPb(from.getEntityValue()); - } - - @Override - protected void setValue(EntityValue from, DatastoreV1.Value.Builder to) { - to.setEntityValue(from.get().toPb()); - } - }; - - public static final class Builder extends - Value.BaseBuilder { - - private Builder() { - super(Type.ENTITY); - } - - @Override - public Builder indexed(boolean indexed) { - // see b/8730533 - Preconditions.checkArgument(!indexed, "EntityValue can't be indexed"); - return super.indexed(indexed); - } - - @Override - public EntityValue build() { - return new EntityValue(this); - } - } - - public EntityValue(PartialEntity entity) { - this(builder(entity)); - } - - private EntityValue(Builder builder) { - super(builder); - } - - @Override - public Builder toBuilder() { - return new Builder().mergeFrom(this); - } - - public static EntityValue of(PartialEntity entity) { - return new EntityValue(entity); - } - - public static Builder builder(PartialEntity entity) { - return new Builder().set(entity).indexed(false); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/KeyFactory.java b/src/main/java/com/google/gcloud/datastore/KeyFactory.java deleted file mode 100644 index 08d854404a3b..000000000000 --- a/src/main/java/com/google/gcloud/datastore/KeyFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.collect.ImmutableList; - -/** - * An helper for creating keys for a specific {@link DatastoreService}, - * using its associated dataset and namespace. - */ -public final class KeyFactory extends BaseKey.Builder { - - private final DatastoreService service; - - public KeyFactory(DatastoreService service) { - super(checkNotNull(service).options().dataset()); - this.service = service; - namespace(service.options().namespace()); - } - - public PartialKey newKey() { - ImmutableList path = ImmutableList.builder() - .addAll(ancestors).add(PathElement.of(kind)).build(); - return new PartialKey(dataset, namespace, path); - } - - public Key newKey(String name) { - ImmutableList path = ImmutableList.builder() - .addAll(ancestors).add(PathElement.of(kind, name)).build(); - return new Key(dataset, namespace, path); - } - - public Key newKey(long id) { - ImmutableList path = ImmutableList.builder() - .addAll(ancestors).add(PathElement.of(kind, id)).build(); - return new Key(dataset, namespace, path); - } - - /** - * Return a key with a newly allocated id. - * @throws DatastoreServiceException if allocation failed. - */ - public Key allocateId() { - return service.allocateId(newKey()); - } - - @Override - protected PartialKey build() { - return newKey(); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/KeyValue.java b/src/main/java/com/google/gcloud/datastore/KeyValue.java deleted file mode 100644 index a97399b5006c..000000000000 --- a/src/main/java/com/google/gcloud/datastore/KeyValue.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.api.services.datastore.DatastoreV1.Value.KEY_VALUE_FIELD_NUMBER; - -import com.google.api.services.datastore.DatastoreV1; - -public final class KeyValue extends Value { - - private static final long serialVersionUID = -1318353707326704821L; - - static final BaseMarshaller MARSHALLER = - new BaseMarshaller() { - - @Override - public int getProtoFieldId() { - return KEY_VALUE_FIELD_NUMBER; - } - - @Override - public Builder newBuilder(Key key) { - return builder(key); - } - - @Override - protected Key getValue(DatastoreV1.Value from) { - return Key.fromPb(from.getKeyValue()); - } - - @Override - protected void setValue(KeyValue from, DatastoreV1.Value.Builder to) { - to.setKeyValue(from.get().toPb()); - } - }; - - public static final class Builder extends Value.BaseBuilder { - - public Builder() { - super(Type.KEY); - } - - @Override - public KeyValue build() { - return new KeyValue(this); - } - } - - public KeyValue(Key key) { - this(builder(key)); - } - - private KeyValue(Builder builder) { - super(builder); - } - - @Override - public Builder toBuilder() { - return new Builder().mergeFrom(this); - } - - public static KeyValue of(Key key) { - return new KeyValue(key); - } - - public static Builder builder(Key key) { - return new Builder().set(key); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/LongValue.java b/src/main/java/com/google/gcloud/datastore/LongValue.java deleted file mode 100644 index 4ab977ed96da..000000000000 --- a/src/main/java/com/google/gcloud/datastore/LongValue.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.api.services.datastore.DatastoreV1.Value.INTEGER_VALUE_FIELD_NUMBER; - -import com.google.api.services.datastore.DatastoreV1; - -public final class LongValue extends Value { - - private static final long serialVersionUID = -8552854340400546861L; - - static final BaseMarshaller MARSHALLER = - new BaseMarshaller() { - - @Override - public int getProtoFieldId() { - return INTEGER_VALUE_FIELD_NUMBER; - } - - @Override - public Builder newBuilder(Long value) { - return builder(value); - } - - @Override - protected Long getValue(DatastoreV1.Value from) { - return from.getIntegerValue(); - } - - @Override - protected void setValue(LongValue from, DatastoreV1.Value.Builder to) { - to.setIntegerValue(from.get()); - } - }; - - public static final class Builder extends Value.BaseBuilder { - - private Builder() { - super(Type.LONG); - } - - @Override - public LongValue build() { - return new LongValue(this); - } - } - - public LongValue(long value) { - this(builder(value)); - } - - private LongValue(Builder builder) { - super(builder); - } - - @Override - public Builder toBuilder() { - return new Builder().mergeFrom(this); - } - - public static LongValue of(long value) { - return new LongValue(value); - } - - public static Builder builder(long value) { - return new Builder().set(value); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/NullValue.java b/src/main/java/com/google/gcloud/datastore/NullValue.java deleted file mode 100644 index af81c017bc72..000000000000 --- a/src/main/java/com/google/gcloud/datastore/NullValue.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.api.services.datastore.DatastoreV1; - -public final class NullValue extends Value { - - private static final long serialVersionUID = 8497300779013002270L; - - static final BaseMarshaller MARSHALLER = - new BaseMarshaller() { - - @Override - public Builder newBuilder(Void value) { - return builder(); - } - - @Override - public int getProtoFieldId() { - return 0; - } - - @Override - protected Void getValue(DatastoreV1.Value from) { - return null; - } - - @Override - protected void setValue(NullValue from, DatastoreV1.Value.Builder to) { - // nothing to set - } - }; - - public static final class Builder extends Value.BaseBuilder { - - private Builder() { - super(Type.NULL); - } - - @Override - public NullValue build() { - return new NullValue(this); - } - - @Override - public Builder set(Void value) { - checkArgument(value == null, "Only null values are allowed"); - return this; - } - } - - public NullValue() { - this(builder()); - } - - private NullValue(Builder builder) { - super(builder); - } - - @Override - public Builder toBuilder() { - return new Builder().mergeFrom(this); - } - - public static NullValue of() { - return new NullValue(); - } - - public static Builder builder() { - return new Builder(); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/PartialEntity.java b/src/main/java/com/google/gcloud/datastore/PartialEntity.java deleted file mode 100644 index df520d3f48b9..000000000000 --- a/src/main/java/com/google/gcloud/datastore/PartialEntity.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.google.gcloud.datastore; - -import com.google.api.services.datastore.DatastoreV1; -import com.google.common.collect.ImmutableSortedMap; -import com.google.protobuf.InvalidProtocolBufferException; - -import java.util.Objects; - -/** - * A partial entity holds one or more properties, represented by a name (as {@link String}) - * and a value (as {@link Value}). - * For a list of possible values see {@link Value.Type}. - * A partial entity also can be associated with a key (partial or full). - * This class is immutable. - */ -public class PartialEntity extends BaseEntity { - - private static final long serialVersionUID = 6492561268709192891L; - - private final transient PartialKey key; - - public static class Builder extends BaseEntity.Builder { - - private PartialKey key; - - private Builder() { - } - - private Builder(PartialEntity entity) { - super(entity); - key = entity.key(); - } - - public Builder key(PartialKey key) { - this.key = key; - return this; - } - - @Override - public PartialEntity build() { - return new PartialEntity(key, ImmutableSortedMap.copyOf(properties)); - } - } - - protected PartialEntity(PartialKey key, ImmutableSortedMap> properties) { - super(properties); - this.key = key; - } - - /** - * Returns a new {@link Entity} with the same properties as this one and - * with the given {@code key}. - */ - public Entity toEntity(Key key) { - return new Entity(key, ImmutableSortedMap.>copyOf(properties())); - } - - /** - * Returns the key for this entity or {@code null} if it does not have one. - */ - public PartialKey key() { - return key; - } - - @Override - public int hashCode() { - return Objects.hash(key, properties()); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof PartialEntity)) { - return false; - } - PartialEntity other = (PartialEntity) obj; - return Objects.equals(key, other.key) - && Objects.equals(properties(), other.properties()); - } - - @Override - protected void populateEntityBuilder(DatastoreV1.Entity.Builder entityPb) { - if (key != null) { - entityPb.setKey(key.toPb()); - } - } - - @Override - protected Object fromPb(byte[] bytesPb) throws InvalidProtocolBufferException { - return fromPb(DatastoreV1.Entity.parseFrom(bytesPb)); - } - - static PartialEntity fromPb(DatastoreV1.Entity entityPb) { - ImmutableSortedMap.Builder> properties = - ImmutableSortedMap.naturalOrder(); - for (DatastoreV1.Property property : entityPb.getPropertyList()) { - properties.put(property.getName(), Value.fromPb(property.getValue())); - } - PartialKey partialKey = null; - if (entityPb.hasKey()) { - partialKey = PartialKey.fromPb(entityPb.getKey()); - if (partialKey instanceof Key) { - return new Entity((Key) partialKey, properties.build()); - } - } - return new PartialEntity(partialKey, properties.build()); - } - - public static Builder builder() { - return new Builder(); - } - - public static Builder builder(PartialKey key) { - return new Builder().key(key); - } - - public static Builder builder(PartialEntity copyFrom) { - return new Builder(copyFrom); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/ProjectionEntity.java b/src/main/java/com/google/gcloud/datastore/ProjectionEntity.java deleted file mode 100644 index 6b219ce0968c..000000000000 --- a/src/main/java/com/google/gcloud/datastore/ProjectionEntity.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.google.gcloud.datastore; - -import com.google.api.services.datastore.DatastoreV1; -import com.google.common.collect.ImmutableSortedMap; -import com.google.protobuf.ByteString; -import com.google.protobuf.InvalidProtocolBufferException; - -import java.util.Objects; - -/** - * A projection entity is a result of a Google Cloud Datastore projection query. - * A projection entity holds one or more properties, represented by a name (as {@link String}) - * and a value (as {@link Value}), and may have a {@link Key}. - * - * @see Google Cloud Datastore projection queries - * @see Google Cloud Datastore Entities, Properties, and Keys - */ -public final class ProjectionEntity extends BaseEntity { - - private static final long serialVersionUID = 432961565733066915L; - - private final Key key; - - static final class Builder extends BaseEntity.Builder { - - private Key key; - - private Builder() { - } - - private Builder(ProjectionEntity entity) { - super(entity); - key = entity.key(); - } - - public Builder key(Key key) { - this.key = key; - return this; - } - - @Override - public ProjectionEntity build() { - return new ProjectionEntity(key, ImmutableSortedMap.copyOf(properties)); - } - } - - ProjectionEntity(Key key, ImmutableSortedMap> properties) { - super(properties); - this.key = key; - } - - @Override - public int hashCode() { - return Objects.hash(key, properties()); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof ProjectionEntity)) { - return false; - } - ProjectionEntity other = (ProjectionEntity) obj; - return Objects.equals(key, other.key) - && Objects.equals(properties(), other.properties()); - } - - public boolean hasKey() { - return key() != null; - } - - /** - * Returns the associated {@link Key} or null if it does not have one. - */ - public Key key() { - return key; - } - - @Override - public DateTime getDateTime(String name) { - Value value = getValue(name); - if (value.hasMeaning() && value.meaning() == 18 && value instanceof LongValue) { - return new DateTime(getLong(name)); - } - return ((DateTimeValue) value).get(); - } - - @Override - public Blob getBlob(String name) { - Value value = getValue(name); - if (value.hasMeaning() && value.meaning() == 18 && value instanceof StringValue) { - return new Blob(ByteString.copyFromUtf8(getString(name)), false); - } - return ((BlobValue) value).get(); - } - - @Override - protected Object fromPb(byte[] bytesPb) throws InvalidProtocolBufferException { - return fromPb(DatastoreV1.Entity.parseFrom(bytesPb)); - } - - static ProjectionEntity fromPb(DatastoreV1.Entity entityPb) { - ImmutableSortedMap.Builder> properties = - ImmutableSortedMap.naturalOrder(); - for (DatastoreV1.Property property : entityPb.getPropertyList()) { - properties.put(property.getName(), Value.fromPb(property.getValue())); - } - Key key = null; - if (entityPb.hasKey()) { - key = Key.fromPb(entityPb.getKey()); - } - return new ProjectionEntity(key, properties.build()); - } - - - public static Builder builder(ProjectionEntity copyFrom) { - return new Builder(copyFrom); - } - - @Override - protected void populateEntityBuilder(DatastoreV1.Entity.Builder entityPb) { - if (key != null) { - entityPb.setKey(key.toPb()); - } - } -} diff --git a/src/main/java/com/google/gcloud/datastore/Query.java b/src/main/java/com/google/gcloud/datastore/Query.java deleted file mode 100644 index c68e3246f62f..000000000000 --- a/src/main/java/com/google/gcloud/datastore/Query.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.api.services.datastore.DatastoreV1; -import com.google.common.base.MoreObjects; -import com.google.common.base.MoreObjects.ToStringHelper; -import com.google.common.collect.Maps; -import com.google.protobuf.GeneratedMessage; -import com.google.protobuf.InvalidProtocolBufferException; - -import java.util.EnumMap; - - -/** - * A Google Cloud Datastore query. - * For usage examples see {@link GqlQuery} and {@link StructuredQuery}. - * - * @param the type of the values returned by this query. - * @see Datastore Queries - */ -public abstract class Query extends Serializable { - - private static final long serialVersionUID = -2748141759901313101L; - - private final Type type; - private final String namespace; - - /** - * This class represents the expected type of the result. - * FULL: A complete {@link Entity}. - * PROJECTION: A partial entity, represented by {@link PartialEntity}. - * KEY_ONLY: An entity's {@link Key}. - */ - public abstract static class Type implements java.io.Serializable { - - private static final long serialVersionUID = 2104157695425806623L; - private static final EnumMap> - PB_TO_INSTANCE = Maps.newEnumMap(DatastoreV1.EntityResult.ResultType.class); - - static final Type UNKNOWN = new Type(null, Object.class) { - - private static final long serialVersionUID = 1602329532153860907L; - - @Override protected Object convert(DatastoreV1.Entity entityPb) { - if (entityPb.getPropertyCount() == 0) { - if (!entityPb.hasKey()) { - return null; - } - return Key.fromPb(entityPb.getKey()); - } - return ProjectionEntity.fromPb(entityPb); - } - }; - - public static final Type FULL = - new Type(DatastoreV1.EntityResult.ResultType.FULL, Entity.class) { - - private static final long serialVersionUID = 7712959777507168274L; - - @Override protected Entity convert(DatastoreV1.Entity entityPb) { - return Entity.fromPb(entityPb); - } - }; - - public static final Type KEY_ONLY = - new Type(DatastoreV1.EntityResult.ResultType.KEY_ONLY, Key.class) { - - private static final long serialVersionUID = -8514289244104446252L; - - @Override protected Key convert(DatastoreV1.Entity entityPb) { - return Key.fromPb(entityPb.getKey()); - } - }; - - public static final Type PROJECTION = new Type( - DatastoreV1.EntityResult.ResultType.PROJECTION, ProjectionEntity.class) { - - private static final long serialVersionUID = -7591409419690650246L; - - @Override protected ProjectionEntity convert(DatastoreV1.Entity entityPb) { - return ProjectionEntity.fromPb(entityPb); - } - }; - - private final Class resultClass; - private final DatastoreV1.EntityResult.ResultType resultType; - - private Type(DatastoreV1.EntityResult.ResultType typePb, Class resultClass) { - this.resultType = typePb; - this.resultClass = checkNotNull(resultClass); - if (typePb != null) { - PB_TO_INSTANCE.put(typePb, this); - } - } - - public Class resultClass() { - return resultClass; - } - - @Override - public int hashCode() { - return resultClass.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Type)) { - return false; - } - Type other = (Type) obj; - return resultClass.equals(other.resultClass); - } - - @Override - public String toString() { - ToStringHelper toStringHelper = MoreObjects.toStringHelper(this); - toStringHelper.add("resultType", resultType); - toStringHelper.add("resultClass", resultClass); - return toStringHelper.toString(); - } - - boolean isAssignableFrom(Type otherType) { - return resultClass.isAssignableFrom(otherType.resultClass); - } - - protected abstract V convert(DatastoreV1.Entity value); - - static Type fromPb(DatastoreV1.EntityResult.ResultType typePb) { - return MoreObjects.firstNonNull(PB_TO_INSTANCE.get(typePb), UNKNOWN); - } - } - - Query(Type type, String namespace) { - this.type = checkNotNull(type); - this.namespace = namespace; - } - - Type type() { - return type; - } - - public String namespace() { - return namespace; - } - - @Override - public String toString() { - ToStringHelper toStringHelper = MoreObjects.toStringHelper(this); - toStringHelper.add("namespace", namespace); - toStringHelper.add("queryPb", super.toString()); - return toStringHelper.toString(); - } - - @Override - protected Object fromPb(byte[] bytesPb) throws InvalidProtocolBufferException { - return fromPb(type, namespace, bytesPb); - } - - protected abstract Object fromPb(Type type, String namespace, byte[] bytesPb) - throws InvalidProtocolBufferException; - - protected abstract void populatePb(DatastoreV1.RunQueryRequest.Builder requestPb); - - protected abstract Query nextQuery(DatastoreV1.QueryResultBatch responsePb); -} diff --git a/src/main/java/com/google/gcloud/datastore/QueryResult.java b/src/main/java/com/google/gcloud/datastore/QueryResult.java deleted file mode 100644 index 3dab5967e81d..000000000000 --- a/src/main/java/com/google/gcloud/datastore/QueryResult.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.google.gcloud.datastore; - -import java.util.Iterator; - -/** - * The result of a Google Cloud Datastore query submission. - * When result is not typed it is possible to cast it to its appropriate type according to - * the {@link #resultClass} value. - * Results are loaded lazily therefore it is possible to get a {@code DatastoreServiceException} - * upon {@link Iterator#hasNext hasNext} or {@link Iterator#next next} calls. - * - * @param V the type of the results value. - */ -public interface QueryResult extends Iterator { - - /** - * Returns the actual class of the result's values. - */ - Class resultClass(); - - /** - * Returns the Cursor for the next result. Not currently implemented (depends on v1beta3). - */ - Cursor cursor(); -} diff --git a/src/main/java/com/google/gcloud/datastore/StringValue.java b/src/main/java/com/google/gcloud/datastore/StringValue.java deleted file mode 100644 index dad18654c8bd..000000000000 --- a/src/main/java/com/google/gcloud/datastore/StringValue.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.api.services.datastore.DatastoreV1.Value.STRING_VALUE_FIELD_NUMBER; -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.api.services.datastore.DatastoreV1; - -public final class StringValue extends Value { - - private static final long serialVersionUID = -3105699707394545523L; - - static final BaseMarshaller MARSHALLER = - new BaseMarshaller() { - - @Override - public int getProtoFieldId() { - return STRING_VALUE_FIELD_NUMBER; - } - - @Override - public Builder newBuilder(String value) { - return builder(value); - } - - @Override - protected String getValue(DatastoreV1.Value from) { - return from.getStringValue(); - } - - @Override - protected void setValue(StringValue from, DatastoreV1.Value.Builder to) { - to.setStringValue(from.get()); - } - }; - - public static final class Builder extends Value.BaseBuilder { - - private Builder() { - super(Type.STRING); - } - - @Override - public StringValue build() { - if (Boolean.TRUE.equals(getIndexed())) { - checkArgument(get().length() <= 500, "Indexed string is limited to 500 characters"); - } - return new StringValue(this); - } - } - - public StringValue(String value) { - this(builder(value)); - } - - private StringValue(Builder builder) { - super(builder); - } - - @Override - public Builder toBuilder() { - return new Builder().mergeFrom(this); - } - - public static StringValue of(String value) { - return new StringValue(value); - } - - public static Builder builder(String value) { - return new Builder().set(value); - } -} diff --git a/src/main/java/com/google/gcloud/datastore/TransactionImpl.java b/src/main/java/com/google/gcloud/datastore/TransactionImpl.java deleted file mode 100644 index 8b43f421b9e5..000000000000 --- a/src/main/java/com/google/gcloud/datastore/TransactionImpl.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.gcloud.datastore.DatastoreServiceException.throwInvalidRequest; - -import com.google.api.services.datastore.DatastoreV1; -import com.google.gcloud.datastore.TransactionOption.IsolationLevel; -import com.google.protobuf.ByteString; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public final class TransactionImpl extends BatchWriterImpl implements Transaction { - - private final ByteString transaction; - private boolean wasRolledback; - - TransactionImpl(DatastoreServiceImpl datastore, TransactionOption... options) { - super(datastore, getBatchOptions(options)); - DatastoreV1.BeginTransactionRequest.Builder requestPb = - DatastoreV1.BeginTransactionRequest.newBuilder(); - Map, TransactionOption> optionsMap = - TransactionOption.asImmutableMap(options); - IsolationLevel isolationLevel = (IsolationLevel) optionsMap.get(IsolationLevel.class); - if (isolationLevel != null) { - requestPb.setIsolationLevel(isolationLevel.level().toPb()); - } - transaction = datastore.requestTransactionId(requestPb); - } - - private static BatchWriteOption[] getBatchOptions(TransactionOption... options) { - List batchOptions = new ArrayList<>(options.length); - for (TransactionOption option : options) { - BatchWriteOption batchOption = option.toBatchWriteOption(); - if (batchOption != null) { - batchOptions.add(batchOption); - } - } - return batchOptions.toArray(new BatchWriteOption[batchOptions.size()]); - } - - @Override - public Entity get(Key key) { - Iterator iter = get(key, DatastoreServiceImpl.EMPTY_KEY_ARRAY); - return iter.hasNext() ? iter.next() : null; - } - - @Override - public Iterator get(Key key, Key... others) { - checkActive(); - DatastoreV1.ReadOptions.Builder readOptionsPb = DatastoreV1.ReadOptions.newBuilder(); - readOptionsPb.setTransaction(transaction); - return datastore.get(readOptionsPb.build(), key, others); - } - - @Override - public QueryResult run(Query query) { - checkActive(); - DatastoreV1.ReadOptions.Builder readOptionsPb = DatastoreV1.ReadOptions.newBuilder(); - readOptionsPb.setTransaction(transaction); - return datastore.run(readOptionsPb.build(), query); - } - - @Override - public void commit() { - submit(); - } - - @Override - public void rollback() { - super.checkActive(); - if (!wasRolledback) { - datastore.rollbackTransaction(transaction); - } - wasRolledback = true; - } - - @Override - public boolean active() { - return super.active() && !wasRolledback; - } - - @Override - protected String getName() { - return "transaction"; - } - - @Override - protected void checkActive() { - super.checkActive(); - if (wasRolledback) { - throwInvalidRequest(getName() + " is not active (was rolledback)"); - } - } - - @Override - protected DatastoreV1.CommitRequest.Builder newCommitRequest() { - DatastoreV1.CommitRequest.Builder requestPb = DatastoreV1.CommitRequest.newBuilder(); - requestPb.setMode(DatastoreV1.CommitRequest.Mode.TRANSACTIONAL); - requestPb.setTransaction(transaction); - return requestPb; - } -} diff --git a/src/main/java/com/google/gcloud/datastore/Validator.java b/src/main/java/com/google/gcloud/datastore/Validator.java deleted file mode 100644 index 2f915ccc3d93..000000000000 --- a/src/main/java/com/google/gcloud/datastore/Validator.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.google.gcloud.datastore; - -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.common.base.Strings; - -import java.util.regex.Pattern; - -/** - * Utility to validate Datastore type/values. - */ -class Validator { - - private static final Pattern DATASET_PATTERN = Pattern.compile( - "([a-z\\d\\-]{1,100}~)?([a-z\\d][a-z\\d\\-\\.]{0,99}\\:)?([a-z\\d][a-z\\d\\-]{0,99})"); - private static final int MAX_NAMESPACE_LENGTH = 100; - private static final Pattern NAMESPACE_PATTERN = - Pattern.compile(String.format("[0-9A-Za-z\\._\\-]{0,%d}", MAX_NAMESPACE_LENGTH)); - - - static String validateDataset(String dataset) { - checkArgument(!Strings.isNullOrEmpty(dataset), "dataset can't be empty or null"); - checkArgument(Validator.DATASET_PATTERN.matcher(dataset).matches(), - "dataset must match the following pattern: " + Validator.DATASET_PATTERN.pattern()); - return dataset; - } - - static String validateNamespace(String namespace) { - if (namespace != null) { - checkArgument(!namespace.isEmpty(), "namespace must not be an empty string"); - checkArgument(namespace.length() <= 100, - "namespace must not contain more than 100 characters"); - checkArgument(Validator.NAMESPACE_PATTERN.matcher(namespace).matches(), - "namespace must the following pattern: " + Validator.NAMESPACE_PATTERN.pattern()); - } - return namespace; - } - - static String validateKind(String kind) { - checkArgument(!Strings.isNullOrEmpty(kind), "kind must not be empty or null"); - checkArgument(kind.length() <= 500, "kind must not contain more than 500 characters"); - return kind; - } -} diff --git a/src/main/java/com/google/gcloud/storage/Acl.java b/src/main/java/com/google/gcloud/storage/Acl.java deleted file mode 100644 index 13bbe2bfb44d..000000000000 --- a/src/main/java/com/google/gcloud/storage/Acl.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.google.gcloud.storage; - -public interface Acl { - - public class ProjectTeam { - // ProjectNumber: The project number. - //ProjectNumber string `json:"projectNumber,omitempty"` - - // Team: The team. Can be owners, editors, or viewers. - //Team string `json:"team,omitempty"` - } - - enum Entity { - USER_ID("user-userId"), - USER_EMAIL("user-emailAddress"), - GROUP_ID("group-groupId"), - GROUP_EMAIL("group-emailAddress"), - ALL_USERS("allUsers"), - ALL_AUTHENTICATED_USERS("allAuthenticatedUsers"); - - private final String value; - - Entity(String value) { - this.value = value; - } - } - - String domain(); - - Entity entity(); - - String entityId(); - - String email(); - - String etag(); - - String generation(); - - - ProjectTeam projectTeam(); - - String role(); -} diff --git a/src/main/java/com/google/gcloud/storage/Bucket.java b/src/main/java/com/google/gcloud/storage/Bucket.java deleted file mode 100644 index 2149cafca6f6..000000000000 --- a/src/main/java/com/google/gcloud/storage/Bucket.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.google.gcloud.storage; - -import java.nio.ByteBuffer; - -public interface Bucket { - - String name(); - - Acl acl(); - - void updateAcl(Acl acl); - - Acl defaultObjectAcl(); - - void updateDefaultObjectAcl(); - - - - - - void delete(Key... objectKey); - - void compose(Iterable source, String dest); - - void copy(String source, String dest); - - // TODO (ozarov): consider replace with Object that has a reference to bucket and name - // that object can return its own meta-data, update its own meta-data, replace its content - // via a stream or byteBuffer, read its content (via stream or ByteBuffer),... - //void copy(String source, String bucket, String dest); - // Also consider read with an offset (and limit). - - void put(String name, ByteBuffer bytes); - - // TODO: add listing -} diff --git a/src/main/java/com/google/gcloud/storage/HttpMethod.java b/src/main/java/com/google/gcloud/storage/HttpMethod.java new file mode 100644 index 000000000000..f5889aedae90 --- /dev/null +++ b/src/main/java/com/google/gcloud/storage/HttpMethod.java @@ -0,0 +1,24 @@ +/* + * Copyright 2015 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.gcloud.storage; + +/** + * + */ +public enum HttpMethod { + GET, HEAD, PUT, POST, DELETE +} diff --git a/src/main/java/com/google/gcloud/storage/Key.java b/src/main/java/com/google/gcloud/storage/Key.java deleted file mode 100644 index 759cc5e9496f..000000000000 --- a/src/main/java/com/google/gcloud/storage/Key.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.google.gcloud.storage; - -public class Key { - - // TODO: add builder, factory method, toURL, from URL, equals,hashCode, toString - private final String bucket; - private final String name; - - /* - Builder() { - - }*/ - - Key(String bucket, String name) { - this.bucket = bucket; - this.name = name; - } - - public String bucket() { - return bucket; - } - - public String name() { - return name; - } -} diff --git a/src/main/java/com/google/gcloud/storage/StorageObject.java b/src/main/java/com/google/gcloud/storage/StorageObject.java deleted file mode 100644 index 0fbee66031b8..000000000000 --- a/src/main/java/com/google/gcloud/storage/StorageObject.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.google.gcloud.storage; - -import java.nio.ByteBuffer; - -public interface StorageObject { - - // builder will have an option to populate content and set acl, bucket, name,.. - - Key key(); - - Acl acl(); - - ByteBuffer content(); - -} diff --git a/src/main/java/com/google/gcloud/storage/StorageService.java b/src/main/java/com/google/gcloud/storage/StorageService.java deleted file mode 100644 index 47e2af985f3a..000000000000 --- a/src/main/java/com/google/gcloud/storage/StorageService.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.google.gcloud.storage; - -public interface StorageService { - - Iterable listBuckets(); - - Bucket getBucket(String bucket); -} diff --git a/src/main/java/com/google/gcloud/storage/StorageServiceFactory.java b/src/main/java/com/google/gcloud/storage/StorageServiceFactory.java deleted file mode 100644 index acc243b9ba38..000000000000 --- a/src/main/java/com/google/gcloud/storage/StorageServiceFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.google.gcloud.storage; - - - -public abstract class StorageServiceFactory { - - private static final StorageServiceFactory INSTANCE = new StorageServiceFactory() { - @Override - public StorageService get(StorageServiceOptions options) { - return new StorageServiceImpl(options); - } - }; - - public static StorageService getDefault(StorageServiceOptions options) { - return INSTANCE.get(options); - } - - public abstract StorageService get(StorageServiceOptions options); -} diff --git a/src/main/java/com/google/gcloud/storage/StorageServiceImpl.java b/src/main/java/com/google/gcloud/storage/StorageServiceImpl.java deleted file mode 100644 index 85c57bea3a9b..000000000000 --- a/src/main/java/com/google/gcloud/storage/StorageServiceImpl.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.google.gcloud.storage; - -import com.google.api.services.storage.Storage; - -final class StorageServiceImpl implements StorageService { - - private final StorageServiceOptions options; - private final Storage storage; - - StorageServiceImpl(StorageServiceOptions options) { - this.options = options; - storage = options.getStorage(); - } - - @Override - public Iterable listBuckets() { - // TODO Auto-generated method stub - return null; - } - - @Override - public Bucket getBucket(String bucket) { - // TODO Auto-generated method stub - return null; - } - - -} diff --git a/src/main/java/com/google/gcloud/storage/StorageServiceOptions.java b/src/main/java/com/google/gcloud/storage/StorageServiceOptions.java deleted file mode 100644 index 3fa48eb5095c..000000000000 --- a/src/main/java/com/google/gcloud/storage/StorageServiceOptions.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.google.gcloud.storage; - -import com.google.api.client.json.jackson.JacksonFactory; -import com.google.api.services.storage.Storage; -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableSet; -import com.google.gcloud.ServiceOptions; - -import java.lang.reflect.Method; -import java.util.Set; - -public class StorageServiceOptions extends ServiceOptions { - - private static final String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control"; - private static final Set SCOPES = ImmutableSet.of(GCS_SCOPE); - private static final String DEFAULT_PATH_DELIMITER = "/"; - - private final String project; - private final String pathDelimiter; - - StorageServiceOptions(Builder builder) { - super(builder); - pathDelimiter = MoreObjects.firstNonNull(builder.pathDelimiter, DEFAULT_PATH_DELIMITER); - project = builder.project != null ? builder.project : getAppEngineProject(); - Preconditions.checkArgument(project != null, "Missing required project id"); - } - - private static String getAppEngineProject() { - // TODO(ozarov): An alternative to reflection would be to depend on AE api jar: - // http://mvnrepository.com/artifact/com.google.appengine/appengine-api-1.0-sdk/1.2.0 - try { - Class factoryClass = - Class.forName("com.google.appengine.api.appidentity.AppIdentityServiceFactory"); - Method method = factoryClass.getMethod("getAppIdentityService"); - Object appIdentityService = method.invoke(null); - method = appIdentityService.getClass().getMethod("getServiceAccountName"); - String serviceAccountName = (String) method.invoke(appIdentityService); - int indexOfAtSign = serviceAccountName.indexOf('@'); - return serviceAccountName.substring(0, indexOfAtSign); - } catch (Exception ex) { - return null; - } - } - - public static class Builder extends ServiceOptions.Builder { - - private String project; - private String pathDelimiter; - - private Builder() { - } - - private Builder(StorageServiceOptions options) { - super(options); - } - - public Builder project(String project) { - this.project = project; - return this; - } - - public Builder pathDelimiter(String pathDelimiter) { - this.pathDelimiter = pathDelimiter; - return this; - } - - @Override - public StorageServiceOptions build() { - return new StorageServiceOptions(this); - } - } - - @Override - protected Set scopes() { - return SCOPES; - } - - Storage getStorage() { - return new Storage.Builder(httpTransport(), new JacksonFactory(), httpRequestInitializer()) - .build(); - } - - public String pathDelimiter() { - return pathDelimiter; - } - - @Override - public Builder toBuilder() { - return new Builder(this); - } - - public static Builder builder() { - return new Builder(); - } - - public static Builder builder(StorageServiceOptions options) { - return new Builder(options); - } -} diff --git a/src/main/java/com/google/gcloud/storage/package-info.java b/src/main/java/com/google/gcloud/storage/package-info.java deleted file mode 100644 index 0f78d1bab46a..000000000000 --- a/src/main/java/com/google/gcloud/storage/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * A client to Google Cloud Storage. - * - * @see Google Cloud Storageg - */ -package com.google.gcloud.storage; diff --git a/src/site/apt/index.apt b/src/site/apt/index.apt new file mode 100644 index 000000000000..395bfe7a65e0 --- /dev/null +++ b/src/site/apt/index.apt @@ -0,0 +1,21 @@ +GCloud Java: Idiomatic Java Client for Google Cloud Platform services. + + This is a Java Client for accessing Google Cloud Platorm services such as Datastore, Storage, PubSub and others. + This library is in a early stage of its development and may occasionally make backwards-incompatible changes, + but it is already usable. + +* Features + + * {{{https://cloud.google.com/datastore/}Google Cloud Datastore}} + +* Links + + * {{{https://github.com/GoogleCloudPlatform/gcloud-java}GitHub repository}} + + * {{{./apidocs/index.html}Javadocs}} + + * {{{https://travis-ci.org/GoogleCloudPlatform/gcloud-java} Continous Integration System (Travis-CI)}} + + * {{{https://github.com/GoogleCloudPlatform/gcloud-java/issues?page=1&state=open}Issues}} + + * {{{https://coveralls.io/r/GoogleCloudPlatform/gcloud-java/}Coverage}} diff --git a/src/site/site.xml b/src/site/site.xml new file mode 100644 index 000000000000..55047ce85c54 --- /dev/null +++ b/src/site/site.xml @@ -0,0 +1,31 @@ + + + + + org.apache.maven.skins + maven-fluido-skin + 1.3.1 + + + + + + + + diff --git a/src/test/java/com/google/gcloud/datastore/DatastoreServiceTest.java b/src/test/java/com/google/gcloud/datastore/DatastoreServiceTest.java deleted file mode 100644 index dd655f884159..000000000000 --- a/src/test/java/com/google/gcloud/datastore/DatastoreServiceTest.java +++ /dev/null @@ -1,620 +0,0 @@ -package com.google.gcloud.datastore; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import com.google.api.services.datastore.DatastoreV1; -import com.google.api.services.datastore.client.Datastore; -import com.google.api.services.datastore.client.DatastoreException; -import com.google.gcloud.datastore.Query.Type; -import com.google.gcloud.datastore.StructuredQuery.OrderBy; -import com.google.gcloud.datastore.StructuredQuery.Projection; -import com.google.gcloud.datastore.StructuredQuery.PropertyFilter; - -import org.easymock.EasyMock; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -@RunWith(JUnit4.class) -public class DatastoreServiceTest { - - private static final String DATASET = "dataset1"; - private static final String KIND1 = "kind1"; - private static final String KIND2 = "kind2"; - private static final NullValue NULL_VALUE = NullValue.of(); - private static final StringValue STR_VALUE = StringValue.of("str"); - private static final BooleanValue BOOL_VALUE = BooleanValue.builder(false).indexed(false).build(); - private static final PartialKey PARTIAL_KEY1 = PartialKey.builder(DATASET, KIND1).build(); - private static final PartialKey PARTIAL_KEY2 = PartialKey.builder(DATASET, KIND2).build(); - private static final Key KEY1 = PARTIAL_KEY1.newKey("name"); - private static final Key KEY2 = Key.builder(KEY1, KIND2, 1).build(); - private static final Key KEY3 = Key.builder(KEY2).name("bla").build(); - private static final Key KEY4 = KEY2.newKey("newName1"); - private static final Key KEY5 = KEY2.newKey("newName2"); - private static final KeyValue KEY_VALUE = KeyValue.of(KEY1); - private static final ListValue LIST_VALUE1 = ListValue.builder() - .addValue(NULL_VALUE) - .addValue(STR_VALUE, BOOL_VALUE) - .build(); - private static final ListValue LIST_VALUE2 = ListValue.of(Collections.singletonList(KEY_VALUE)); - private static final DateTimeValue DATE_TIME_VALUE = new DateTimeValue(DateTime.now()); - private static final PartialEntity PARTIAL_ENTITY1 = PartialEntity.builder(PARTIAL_KEY2) - .set("str", STR_VALUE).set("bool", BOOL_VALUE).set("list", LIST_VALUE1).build(); - private static final PartialEntity PARTIAL_ENTITY2 = PartialEntity.builder(PARTIAL_ENTITY1) - .remove("str").set("bool", true).set("list", LIST_VALUE1.get()).build(); - private static final Entity ENTITY1 = Entity.builder(KEY1) - .set("str", STR_VALUE) - .set("date", DATE_TIME_VALUE) - .set("bool", BOOL_VALUE) - .set("partial1", EntityValue.of(PARTIAL_ENTITY1)) - .set("list", LIST_VALUE2) - .build(); - private static final Entity ENTITY2 = Entity.builder(ENTITY1).key(KEY2).remove("str") - .set("name", "koko").setNull("null").set("age", 20).build(); - private static final Entity ENTITY3 = Entity.builder(ENTITY1).key(KEY3).remove("str") - .set("null", NULL_VALUE).set("partial1", PARTIAL_ENTITY2).set("partial2", ENTITY2).build(); - - private DatastoreServiceOptions options; - private DatastoreService datastore; - private DatastoreHelper helper; - - @Before - public void setUp() { - // TODO(ozarov): document that this test depends on a local gcd running. - // Unfortunately, the gcd tool is not bundled with the cloud SDK and need - // to be downloaded independently from - // https://cloud.google.com/datastore/docs/tools/devserver (b/16372095). - // To start the gcd run: - // gcd.sh create dataset1; gcd.sh start dataset1 - // We should have an option to start the gcd from maven/ant. - options = DatastoreServiceOptions.builder() - .dataset(DATASET) - .host("http://localhost:8080") - .build(); - datastore = DatastoreServiceFactory.getDefault(options); - helper = DatastoreHelper.createFor(datastore); - // Prepare data for testing - datastore.delete(KEY1, KEY2, KEY3, KEY4, KEY5); - datastore.add(ENTITY1, ENTITY2); - } - - @Test - public void testGetOptions() { - assertSame(options, datastore.options()); - } - - @Test - public void testNewTransactionCommit() { - Transaction transaction = datastore.newTransaction(); - transaction.add(ENTITY3); - Entity entity2 = Entity.builder(ENTITY2) - .clear() - .setNull("bla") - .build(); - transaction.update(entity2); - transaction.delete(KEY1); - transaction.commit(); - - List list = helper.fetch(KEY1, KEY2, KEY3); - assertNull(list.get(0)); - assertEquals(entity2, list.get(1)); - assertEquals(ENTITY3, list.get(2)); - assertEquals(3, list.size()); - - try { - transaction.commit(); - fail("Expecting a failure"); - } catch (DatastoreServiceException ex) { - // expected to fail - } - - try { - transaction.rollback(); - fail("Expecting a failure"); - } catch (DatastoreServiceException ex) { - // expected to fail - } - - verifyNotUsable(transaction); - } - - @Test - public void testTransactionWithRead() { - Transaction transaction = datastore.newTransaction(); - assertNull(transaction.get(KEY3)); - transaction.add(ENTITY3); - transaction.commit(); - assertEquals(ENTITY3, datastore.get(KEY3)); - - transaction = datastore.newTransaction(); - assertEquals(ENTITY3, transaction.get(KEY3)); - // update entity3 during the transaction - datastore.put(Entity.builder(ENTITY3).clear().build()); - transaction.update(ENTITY2); - try { - transaction.commit(); - fail("Expecting a failure"); - } catch (DatastoreServiceException expected) { - expected.printStackTrace(); - assertEquals(DatastoreServiceException.Code.ABORTED, expected.code()); - } - } - - @Test - public void testTransactionWithQuery() { - Query query = - StructuredQuery.builder().kind(KIND2).filter(PropertyFilter.hasAncestor(KEY2)).build(); - Transaction transaction = datastore.newTransaction(); - QueryResult results = transaction.run(query); - assertEquals(ENTITY2, results.next()); - assertFalse(results.hasNext()); - transaction.add(ENTITY3); - transaction.commit(); - assertEquals(ENTITY3, datastore.get(KEY3)); - - transaction = datastore.newTransaction(); - results = transaction.run(query); - assertEquals(ENTITY2, results.next()); - transaction.delete(ENTITY3.key()); - // update entity2 during the transaction - datastore.put(Entity.builder(ENTITY2).clear().build()); - try { - transaction.commit(); - fail("Expecting a failure"); - } catch (DatastoreServiceException expected) { - expected.printStackTrace(); - assertEquals(DatastoreServiceException.Code.ABORTED, expected.code()); - } - } - - @Test - public void testNewTransactionRollback() { - Transaction transaction = datastore.newTransaction(); - transaction.add(ENTITY3); - Entity entity2 = Entity.builder(ENTITY2).clear().setNull("bla") - .set("list3", StringValue.of("bla"), StringValue.builder("bla").build()).build(); - transaction.update(entity2); - transaction.delete(KEY1); - transaction.rollback(); - transaction.rollback(); // should be safe to repeat rollback calls - - try { - transaction.commit(); - fail("Expecting a failure"); - } catch (DatastoreServiceException ex) { - // expected to fail - } - - verifyNotUsable(transaction); - - List list = helper.fetch(KEY1, KEY2, KEY3); - assertEquals(ENTITY1, list.get(0)); - assertEquals(ENTITY2, list.get(1)); - assertNull(list.get(2)); - assertEquals(3, list.size()); - } - - private void verifyNotUsable(DatastoreWriter writer) { - try { - writer.add(ENTITY3); - fail("Expecting a failure"); - } catch (DatastoreServiceException ex) { - // expected to fail - } - - try { - writer.put(ENTITY3); - fail("Expecting a failure"); - } catch (DatastoreServiceException ex) { - // expected to fail - } - - try { - writer.update(ENTITY3); - fail("Expecting a failure"); - } catch (DatastoreServiceException ex) { - // expected to fail - } - - try { - writer.delete(ENTITY3.key()); - fail("Expecting a failure"); - } catch (DatastoreServiceException ex) { - // expected to fail - } - } - - @Test - public void testNewBatchWriter() { - BatchWriter batchWriter = datastore.newBatchWriter(); - Entity entity1 = Entity.builder(ENTITY1).clear().build(); - Entity entity2 = Entity.builder(ENTITY2).clear().setNull("bla").build(); - Entity entity4 = Entity.builder(KEY4).set("value", StringValue.of("value")).build(); - Entity entity5 = Entity.builder(KEY5).set("value", "value").build(); - - batchWriter.add(entity4, entity5); - batchWriter.put(ENTITY3, entity1, entity2); - batchWriter.submit(); - Iterator entities = - helper.fetch(KEY1, KEY2, KEY3, entity4.key(), entity5.key()).iterator(); - assertEquals(entity1, entities.next()); - assertEquals(entity2, entities.next()); - assertEquals(ENTITY3, entities.next()); - assertEquals(entity4, entities.next()); - assertEquals(entity5, entities.next()); - assertFalse(entities.hasNext()); - - try { - batchWriter.submit(); - fail("Expecting a failure"); - } catch (DatastoreServiceException ex) { - // expected to fail - } - verifyNotUsable(batchWriter); - - batchWriter = datastore.newBatchWriter(); - batchWriter.delete(entity4.key(), entity5.key()); - batchWriter.update(ENTITY1, ENTITY2, ENTITY3); - batchWriter.submit(); - entities = helper.fetch(KEY1, KEY2, KEY3, entity4.key(), entity5.key()).iterator(); - assertEquals(ENTITY1, entities.next()); - assertEquals(ENTITY2, entities.next()); - assertEquals(ENTITY3, entities.next()); - assertNull(entities.next()); - assertNull(entities.next()); - assertFalse(entities.hasNext()); - - // TODO need to cover the following use-cases: - // delete after put/add/update - // put after delete/add/update - // update after delete/add/put - // add after delete/update/put - } - - @Test - public void testRunGqlQueryNoCasting() throws DatastoreException { - Query query1 = GqlQuery.builder(Type.FULL, "select * from " + KIND1).build(); - QueryResult results1 = datastore.run(query1); - assertTrue(results1.hasNext()); - assertEquals(ENTITY1, results1.next()); - assertFalse(results1.hasNext()); - - datastore.put(ENTITY3); - Query query2 = GqlQuery.builder( - Type.FULL, "select * from " + KIND2 + " order by __key__").build(); - QueryResult results2 = datastore.run(query2); - assertTrue(results2.hasNext()); - assertEquals(ENTITY2, results2.next()); - assertTrue(results2.hasNext()); - assertEquals(ENTITY3, results2.next()); - assertFalse(results2.hasNext()); - - query1 = GqlQuery.builder(Type.FULL, "select * from bla").build(); - results1 = datastore.run(query1); - assertFalse(results1.hasNext()); - - Query keyOnlyQuery = - GqlQuery.builder(Type.KEY_ONLY, "select __key__ from " + KIND1).build(); - QueryResult keyOnlyResults = datastore.run(keyOnlyQuery); - assertTrue(keyOnlyResults.hasNext()); - assertEquals(KEY1, keyOnlyResults.next()); - assertFalse(keyOnlyResults.hasNext()); - - GqlQuery keyProjectionQuery = GqlQuery.builder( - Type.PROJECTION, "select __key__ from " + KIND1).build(); - QueryResult keyProjectionResult = datastore.run(keyProjectionQuery); - assertTrue(keyProjectionResult.hasNext()); - ProjectionEntity projectionEntity = keyProjectionResult.next(); - assertEquals(KEY1, projectionEntity.key()); - assertTrue(projectionEntity.properties().isEmpty()); - assertFalse(keyProjectionResult.hasNext()); - - GqlQuery projectionQuery = GqlQuery.builder( - Type.PROJECTION, "select str, date from " + KIND1).build(); - - // this hack is needed because of b/18806697 - DatastoreV1.RunQueryRequest.Builder requestPb = DatastoreV1.RunQueryRequest.newBuilder(); - requestPb.setGqlQuery(projectionQuery.toPb()); - requestPb.setPartitionId(DatastoreV1.PartitionId.newBuilder().setDatasetId(DATASET).build()); - DatastoreV1.RunQueryResponse responsePb = - ((DatastoreServiceImpl) datastore).runQuery(requestPb.build()); - DatastoreV1.RunQueryResponse.Builder responsePbBuilder = responsePb.toBuilder(); - responsePbBuilder.getBatchBuilder() - .setEntityResultType(DatastoreV1.EntityResult.ResultType.PROJECTION).build(); - Datastore mockDatastore = EasyMock.createMock(Datastore.class); - DatastoreV1.EntityResult found = - DatastoreV1.EntityResult.newBuilder().setEntity(ENTITY1.toPb()).build(); - EasyMock.expect(mockDatastore.lookup(EasyMock.anyObject())) - .andReturn(DatastoreV1.LookupResponse.newBuilder().addFound(found).build()); - EasyMock.expect(mockDatastore.runQuery(requestPb.build())).andReturn(responsePbBuilder.build()); - EasyMock.replay(mockDatastore); - datastore = DatastoreServiceFactory.getDefault( - datastore.options().toBuilder().datastore(mockDatastore).build()); - // end of hack - - QueryResult projectionResult = datastore.run(projectionQuery); - assertTrue(projectionResult.hasNext()); - projectionEntity = projectionResult.next(); - assertEquals("str", projectionEntity.getString("str")); - assertEquals(DATE_TIME_VALUE.get(), projectionEntity.getDateTime("date")); - assertEquals(DATE_TIME_VALUE.get().timestampMicroseconds(), - projectionEntity.getLong("date")); - assertEquals(2, projectionEntity.names().size()); - assertFalse(projectionResult.hasNext()); - EasyMock.verify(mockDatastore); - } - - @Test - public void testRunGqlQueryWithCasting() { - @SuppressWarnings("unchecked") - Query query1 = (Query) GqlQuery.builder("select * from " + KIND1).build(); - QueryResult results1 = datastore.run(query1); - assertTrue(results1.hasNext()); - assertEquals(ENTITY1, results1.next()); - assertFalse(results1.hasNext()); - - Query query2 = GqlQuery.builder("select * from " + KIND1).build(); - QueryResult results2 = datastore.run(query2); - assertEquals(Entity.class, results2.resultClass()); - @SuppressWarnings("unchecked") - QueryResult results3 = (QueryResult) results2; - assertTrue(results3.hasNext()); - assertEquals(ENTITY1, results3.next()); - assertFalse(results3.hasNext()); - } - - @Test - public void testRunStructuredQuery() throws DatastoreException { - StructuredQuery query = - StructuredQuery.builder().kind(KIND1).orderBy(OrderBy.asc("__key__")).build(); - QueryResult results1 = datastore.run(query); - assertTrue(results1.hasNext()); - assertEquals(ENTITY1, results1.next()); - assertFalse(results1.hasNext()); - - StructuredQuery keyOnlyQuery = StructuredQuery.keyOnlyBuilder().kind(KIND1).build(); - QueryResult results2 = datastore.run(keyOnlyQuery); - assertTrue(results2.hasNext()); - assertEquals(ENTITY1.key(), results2.next()); - assertFalse(results2.hasNext()); - - StructuredQuery keyOnlyProjectionQuery = StructuredQuery.projectionBuilder() - .kind(KIND1).projection(Projection.property("__key__")).build(); - QueryResult results3 = datastore.run(keyOnlyProjectionQuery); - assertTrue(results3.hasNext()); - ProjectionEntity projectionEntity = results3.next(); - assertEquals(ENTITY1.key(), projectionEntity.key()); - assertTrue(projectionEntity.names().isEmpty()); - assertFalse(results2.hasNext()); - - StructuredQuery projectionQuery = StructuredQuery.projectionBuilder() - .kind(KIND2) - .projection(Projection.property("age"), Projection.first("name")) - .filter(PropertyFilter.gt("age", 18)) - .groupBy("age") - .orderBy(OrderBy.asc("age")) - .limit(10) - .build(); - // this hack is needed because of b/18806697 - DatastoreV1.RunQueryRequest.Builder requestPb = DatastoreV1.RunQueryRequest.newBuilder(); - requestPb.setQuery(projectionQuery.toPb()); - requestPb.setPartitionId(DatastoreV1.PartitionId.newBuilder().setDatasetId(DATASET).build()); - DatastoreV1.RunQueryResponse responsePb = - ((DatastoreServiceImpl) datastore).runQuery(requestPb.build()); - DatastoreV1.RunQueryResponse.Builder responsePbBuilder = responsePb.toBuilder(); - responsePbBuilder.getBatchBuilder() - .setEntityResultType(DatastoreV1.EntityResult.ResultType.PROJECTION).build(); - Datastore mockDatastore = EasyMock.createMock(Datastore.class); - DatastoreV1.EntityResult missing = - DatastoreV1.EntityResult.newBuilder().setEntity(ENTITY1.toPb()).build(); - EasyMock.expect(mockDatastore.lookup(EasyMock.anyObject())) - .andReturn(DatastoreV1.LookupResponse.newBuilder().addMissing(missing).build()); - EasyMock.expect(mockDatastore.runQuery(requestPb.build())).andReturn(responsePbBuilder.build()); - EasyMock.replay(mockDatastore); - datastore = DatastoreServiceFactory.getDefault( - datastore.options().toBuilder().datastore(mockDatastore).build()); - // end of hack - - QueryResult results4 = datastore.run(projectionQuery); - assertTrue(results4.hasNext()); - ProjectionEntity entity = results4.next(); - assertEquals(ENTITY2.key(), entity.key()); - assertEquals(20, entity.getLong("age")); - assertEquals("koko", entity.getString("name")); - assertEquals(2, entity.properties().size()); - assertFalse(results4.hasNext()); - EasyMock.verify(mockDatastore); - - // TODO(ozarov): construct a test to verify nextQuery/pagination - } - - @Test - public void testAllocateId() { - KeyFactory keyFactory = helper.newKeyFactory().kind(KIND1); - PartialKey pk1 = keyFactory.newKey(); - Key key1 = keyFactory.allocateId(); - assertEquals(key1.dataset(), pk1.dataset()); - assertEquals(key1.namespace(), pk1.namespace()); - assertEquals(key1.ancestors(), pk1.ancestors()); - assertEquals(key1.kind(), pk1.kind()); - assertTrue(key1.hasId()); - assertFalse(key1.hasName()); - assertEquals(pk1.newKey(key1.id()), key1); - - Key key2 = datastore.allocateId(pk1); - assertNotEquals(key1, key2); - assertEquals(pk1.newKey(key2.id()), key2); - - Key key3 = datastore.allocateId(key1); - assertNotEquals(key1, key3); - assertEquals(pk1.newKey(key3.id()), key3); - } - - @Test - public void testAllocateIdArray() { - KeyFactory keyFactory = helper.newKeyFactory().kind(KIND1); - PartialKey partialKey1 = keyFactory.newKey(); - PartialKey partialKey2 = keyFactory.kind(KIND2).ancestors(PathElement.of(KIND1, 10)).newKey(); - Key key3 = keyFactory.newKey("name"); - Key key4 = keyFactory.newKey(1); - Iterator result = - datastore.allocateId(partialKey1, partialKey2, key3, key4, partialKey1, key3); - Map map = new HashMap<>(); - int count = 0; - while (result.hasNext()) { - map.put(++count, result.next()); - } - assertEquals(6, map.size()); - assertEquals(partialKey1.newKey(map.get(1).id()), map.get(1)); - assertEquals(partialKey1.newKey(map.get(5).id()), map.get(5)); - assertEquals(partialKey2.newKey(map.get(2).id()), map.get(2)); - assertEquals(Key.builder(key3).id(map.get(3).id()).build(), map.get(3)); - assertEquals(Key.builder(key3).id(map.get(6).id()).build(), map.get(6)); - assertEquals(Key.builder(key4).id(map.get(4).id()).build(), map.get(4)); - } - - @Test - public void testGet() { - Entity entity = datastore.get(KEY3); - assertNull(entity); - - entity = datastore.get(KEY1); - assertEquals(ENTITY1, entity); - StringValue value1 = entity.getValue("str"); - assertEquals(STR_VALUE, value1); - BooleanValue value2 = entity.getValue("bool"); - assertEquals(BOOL_VALUE, value2); - ListValue value3 = entity.getValue("list"); - assertEquals(LIST_VALUE2, value3); - DateTimeValue value4 = entity.getValue("date"); - assertEquals(DATE_TIME_VALUE, value4); - PartialEntity value5 = entity.getEntity("partial1"); - assertEquals(PARTIAL_ENTITY1, value5); - assertEquals(5, entity.names().size()); - assertFalse(entity.contains("bla")); - } - - @Test - public void testGetArray() { - datastore.put(ENTITY3); - Iterator result = - helper.fetch(KEY1, Key.builder(KEY1).name("bla").build(), KEY2, KEY3).iterator(); - assertEquals(ENTITY1, result.next()); - assertNull(result.next()); - assertEquals(ENTITY2, result.next()); - Entity entity3 = result.next(); - assertEquals(ENTITY3, entity3); - assertTrue(entity3.isNull("null")); - assertEquals(false, entity3.getBoolean("bool")); - assertEquals(LIST_VALUE2.get(), entity3.getList("list")); - PartialEntity partial1 = entity3.getEntity("partial1"); - Entity partial2 = (Entity) entity3.getEntity("partial2"); - assertEquals(partial1, PARTIAL_ENTITY2); - assertEquals(partial2, ENTITY2); - assertEquals(Value.Type.BOOLEAN, entity3.getValue("bool").type()); - assertEquals(6, entity3.names().size()); - assertFalse(entity3.contains("bla")); - try { - entity3.getString("str"); - fail("Expecting a failure"); - } catch (DatastoreServiceException expected) { - // expected - no such property - } - assertFalse(result.hasNext()); - // TODO(ozarov): construct a test to verify more results - } - - @Test - public void testAdd() { - List keys = helper.fetch(ENTITY1.key(), ENTITY3.key()); - assertEquals(ENTITY1, keys.get(0)); - assertNull(keys.get(1)); - assertEquals(2, keys.size()); - - try { - datastore.add(ENTITY1); - fail("Expecting a failure"); - } catch (DatastoreServiceException expected) { - // expected; - } - datastore.add(ENTITY3); - assertEquals(ENTITY3, datastore.get(ENTITY3.key())); - } - - @Test - public void testUpdate() { - List keys = helper.fetch(ENTITY1.key(), ENTITY3.key()); - assertEquals(ENTITY1, keys.get(0)); - assertNull(keys.get(1)); - assertEquals(2, keys.size()); - - try { - datastore.update(ENTITY3); - fail("Expecting a failure"); - } catch (DatastoreServiceException expected) { - // expected; - } - datastore.add(ENTITY3); - assertEquals(ENTITY3, datastore.get(ENTITY3.key())); - Entity entity3 = Entity.builder(ENTITY3).clear().set("bla", new NullValue()).build(); - assertNotEquals(ENTITY3, entity3); - datastore.update(entity3); - assertEquals(entity3, datastore.get(ENTITY3.key())); - } - - @Test - public void testPut() { - Iterator keys = helper.fetch(ENTITY1.key(), ENTITY2.key(), ENTITY3.key()).iterator(); - assertEquals(ENTITY1, keys.next()); - assertEquals(ENTITY2, keys.next()); - assertNull(keys.next()); - assertFalse(keys.hasNext()); - - Entity entity2 = Entity.builder(ENTITY2).clear().set("bla", new NullValue()).build(); - assertNotEquals(ENTITY2, entity2); - datastore.put(ENTITY3, ENTITY1, entity2); - keys = helper.fetch(ENTITY1.key(), ENTITY2.key(), ENTITY3.key()).iterator(); - assertEquals(ENTITY1, keys.next()); - assertEquals(entity2, keys.next()); - assertEquals(ENTITY3, keys.next()); - assertFalse(keys.hasNext()); - } - - @Test - public void testDelete() { - Iterator keys = helper.fetch(ENTITY1.key(), ENTITY2.key(), ENTITY3.key()).iterator(); - assertEquals(ENTITY1, keys.next()); - assertEquals(ENTITY2, keys.next()); - assertNull(keys.next()); - assertFalse(keys.hasNext()); - datastore.delete(ENTITY1.key(), ENTITY2.key(), ENTITY3.key()); - keys = helper.fetch(ENTITY1.key(), ENTITY2.key(), ENTITY3.key()).iterator(); - assertNull(keys.next()); - assertNull(keys.next()); - assertNull(keys.next()); - assertFalse(keys.hasNext()); - } - - @Test - public void testKeyFactory() { - KeyFactory keyFactory = new KeyFactory(datastore).kind(KIND1); - assertEquals(PARTIAL_KEY1, keyFactory.newKey()); - assertEquals(PartialKey.builder(PARTIAL_KEY1).kind(KIND2).build(), - new KeyFactory(datastore).kind(KIND2).newKey()); - assertEquals(KEY1, keyFactory.newKey("name")); - assertEquals(Key.builder(KEY1).id(2).build(), keyFactory.newKey(2)); - } -}