diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..d81d43b1a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,51 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +Thanks for stopping by to let us know something could be better! + +**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. + +Please run down the following list and make sure you've tried the usual "quick fixes": + + - Search the issues already opened: https://github.com/googleapis/google-cloud-core/issues + - Check for answers on StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform + +If you are still having issues, please include as much information as possible: + +#### Environment details + +1. Specify the API at the beginning of the title. For example, "BigQuery: ..."). + General, Core, and Other are also allowed as types +2. OS type and version: +3. Java version: +4. google-cloud-core version(s): + +#### Steps to reproduce + + 1. ? + 2. ? + +#### Code example + +```java +// example +``` + +#### Stack trace +``` +Any relevant stacktrace here. +``` + +#### External references such as API reference guides + +- ? + +#### Any additional information below + + +Following these steps guarantees the quickest resolution possible. + +Thanks! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..754e30c68a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Suggest an idea for this library + +--- + +Thanks for stopping by to let us know something could be better! + +**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. + +**Is your feature request related to a problem? Please describe.** +What the problem is. Example: I'm always frustrated when [...] + +**Describe the solution you'd like** +What you want to happen. + +**Describe alternatives you've considered** +Any alternative solutions or features you've considered. + +**Additional context** +Any other context or screenshots about the feature request. diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md new file mode 100644 index 0000000000..9958690321 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support_request.md @@ -0,0 +1,7 @@ +--- +name: Support request +about: If you have a support contract with Google, please create an issue in the Google Cloud Support console. + +--- + +**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..0bd0ee0620 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1 @@ +Fixes # (it's a good idea to open an issue first for context and/or discussion) \ No newline at end of file diff --git a/.github/release-please.yml b/.github/release-please.yml new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/.github/release-please.yml @@ -0,0 +1 @@ + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..fadd6afc2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Maven +target/ + +# Eclipse +.classpath +.project +.settings + +# Intellij +*.iml +.idea/ + +# python utilities +*.pyc +__pycache__ diff --git a/.kokoro/build.bat b/.kokoro/build.bat new file mode 100644 index 0000000000..bcca1f45d0 --- /dev/null +++ b/.kokoro/build.bat @@ -0,0 +1,3 @@ +:: See documentation in type-shell-output.bat + +"C:\Program Files\Git\bin\bash.exe" github/java-core/.kokoro/build.sh diff --git a/.kokoro/build.sh b/.kokoro/build.sh new file mode 100755 index 0000000000..2ffb5ef7f0 --- /dev/null +++ b/.kokoro/build.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Copyright 2019 Google LLC +# +# 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. + +set -eo pipefail + +## Get the directory of the build script +scriptDir=$(realpath $(dirname "${BASH_SOURCE[0]}")) +## cd to the parent directory, i.e. the root of the git repo +cd ${scriptDir}/.. + +# Print out Java version +java -version +echo ${JOB_TYPE} + +mvn install -B -V \ + -DskipTests=true \ + -Dmaven.javadoc.skip=true \ + -Dgcloud.download.skip=true \ + -T 1C + +# if GOOGLE_APPLICATION_CREDIENTIALS is specified as a relative path prepend Kokoro root directory onto it +if [[ ! -z "${GOOGLE_APPLICATION_CREDENTIALS}" && "${GOOGLE_APPLICATION_CREDENTIALS}" != /* ]]; then + export GOOGLE_APPLICATION_CREDENTIALS=$(realpath ${KOKORO_ROOT}/src/${GOOGLE_APPLICATION_CREDENTIALS}) +fi + +case ${JOB_TYPE} in +test) + mvn test -B + bash ${KOKORO_GFILE_DIR}/codecov.sh + ;; +lint) + mvn com.coveo:fmt-maven-plugin:check + ;; +javadoc) + mvn javadoc:javadoc javadoc:test-javadoc + ;; +integration) + mvn -B ${INTEGRATION_TEST_ARGS} -DtrimStackTrace=false -fae verify + ;; +*) + ;; +esac \ No newline at end of file diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg new file mode 100644 index 0000000000..054c30c8f7 --- /dev/null +++ b/.kokoro/common.cfg @@ -0,0 +1,13 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Download trampoline resources. These will be in ${KOKORO_GFILE_DIR} +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# All builds use the trampoline script to run in docker. +build_file: "java-core/.kokoro/trampoline.sh" + +# Tell the trampoline which build file to use. +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/build.sh" +} diff --git a/.kokoro/continuous/common.cfg b/.kokoro/continuous/common.cfg new file mode 100644 index 0000000000..af5a907dc4 --- /dev/null +++ b/.kokoro/continuous/common.cfg @@ -0,0 +1,24 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "java-core/.kokoro/trampoline.sh" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/build.sh" +} + +env_vars: { + key: "JOB_TYPE" + value: "test" +} diff --git a/.kokoro/continuous/dependencies.cfg b/.kokoro/continuous/dependencies.cfg new file mode 100644 index 0000000000..895832a928 --- /dev/null +++ b/.kokoro/continuous/dependencies.cfg @@ -0,0 +1,12 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/dependencies.sh" +} diff --git a/.kokoro/continuous/integration.cfg b/.kokoro/continuous/integration.cfg new file mode 100644 index 0000000000..3b017fc80f --- /dev/null +++ b/.kokoro/continuous/integration.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} diff --git a/.kokoro/continuous/java11.cfg b/.kokoro/continuous/java11.cfg new file mode 100644 index 0000000000..709f2b4c73 --- /dev/null +++ b/.kokoro/continuous/java11.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java11" +} diff --git a/.kokoro/continuous/java7.cfg b/.kokoro/continuous/java7.cfg new file mode 100644 index 0000000000..cb24f44eea --- /dev/null +++ b/.kokoro/continuous/java7.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java7" +} diff --git a/.kokoro/continuous/java8-osx.cfg b/.kokoro/continuous/java8-osx.cfg new file mode 100644 index 0000000000..8b617e2594 --- /dev/null +++ b/.kokoro/continuous/java8-osx.cfg @@ -0,0 +1,3 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +build_file: "java-core/.kokoro/build.sh" diff --git a/.kokoro/continuous/java8-win.cfg b/.kokoro/continuous/java8-win.cfg new file mode 100644 index 0000000000..1a6311a420 --- /dev/null +++ b/.kokoro/continuous/java8-win.cfg @@ -0,0 +1,3 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +build_file: "java-core/.kokoro/build.bat" diff --git a/.kokoro/continuous/java8.cfg b/.kokoro/continuous/java8.cfg new file mode 100644 index 0000000000..3b017fc80f --- /dev/null +++ b/.kokoro/continuous/java8.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} diff --git a/.kokoro/continuous/lint.cfg b/.kokoro/continuous/lint.cfg new file mode 100644 index 0000000000..6d323c8ae7 --- /dev/null +++ b/.kokoro/continuous/lint.cfg @@ -0,0 +1,13 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. + +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} + +env_vars: { + key: "JOB_TYPE" + value: "lint" +} \ No newline at end of file diff --git a/.kokoro/continuous/propose_release.cfg b/.kokoro/continuous/propose_release.cfg new file mode 100644 index 0000000000..fc798ae888 --- /dev/null +++ b/.kokoro/continuous/propose_release.cfg @@ -0,0 +1,53 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "java-core/.kokoro/trampoline.sh" + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/continuous/propose_release.sh" +} + +# tokens used by release-please to keep an up-to-date release PR. +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "github-magic-proxy-key-release-please" + } + } +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "github-magic-proxy-token-release-please" + } + } +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "github-magic-proxy-url-release-please" + } + } +} diff --git a/.kokoro/continuous/propose_release.sh b/.kokoro/continuous/propose_release.sh new file mode 100755 index 0000000000..1fd8acb1bf --- /dev/null +++ b/.kokoro/continuous/propose_release.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Copyright 2019 Google LLC +# +# 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 +# +# https://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. + +set -eo pipefail + +export NPM_CONFIG_PREFIX=/home/node/.npm-global + +if [ -f ${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-url-release-please ]; then + # Groom the release PR as new commits are merged. + npx release-please release-pr --token=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-token-release-please \ + --repo-url=googleapis/java-core \ + --package-name="google-cloud-core" \ + --api-url=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-url-release-please \ + --proxy-key=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-key-release-please \ + --release-type=java-yoshi +fi diff --git a/.kokoro/dependencies.sh b/.kokoro/dependencies.sh new file mode 100755 index 0000000000..51e976d441 --- /dev/null +++ b/.kokoro/dependencies.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Copyright 2019 Google LLC +# +# 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. + +set -eo pipefail + +cd github/java-core/ + +# Print out Java +java -version +echo $JOB_TYPE + +export MAVEN_OPTS="-Xmx1024m -XX:MaxPermSize=128m" + +mvn install -DskipTests=true -B -V +mvn -B dependency:analyze -DfailOnWarning=true diff --git a/.kokoro/linkage-monitor.sh b/.kokoro/linkage-monitor.sh new file mode 100755 index 0000000000..ec3da4ec35 --- /dev/null +++ b/.kokoro/linkage-monitor.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Copyright 2019 Google LLC +# +# 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. + +set -eo pipefail +# Display commands being run. +set -x + +cd github/java-core/ + +# Print out Java version +java -version +echo ${JOB_TYPE} + +mvn install -DskipTests=true -Dmaven.javadoc.skip=true -Dgcloud.download.skip=true -B -V + +# Kokoro job cloud-opensource-java/ubuntu/linkage-monitor-gcs creates this JAR +JAR=linkage-monitor-latest-all-deps.jar +curl -v -O "https://storage.googleapis.com/cloud-opensource-java-linkage-monitor/${JAR}" + +# Fails if there's new linkage errors compared with baseline +java -jar ${JAR} com.google.cloud:libraries-bom diff --git a/.kokoro/nightly/common.cfg b/.kokoro/nightly/common.cfg new file mode 100644 index 0000000000..af5a907dc4 --- /dev/null +++ b/.kokoro/nightly/common.cfg @@ -0,0 +1,24 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "java-core/.kokoro/trampoline.sh" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/build.sh" +} + +env_vars: { + key: "JOB_TYPE" + value: "test" +} diff --git a/.kokoro/nightly/dependencies.cfg b/.kokoro/nightly/dependencies.cfg new file mode 100644 index 0000000000..895832a928 --- /dev/null +++ b/.kokoro/nightly/dependencies.cfg @@ -0,0 +1,12 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/dependencies.sh" +} diff --git a/.kokoro/nightly/integration.cfg b/.kokoro/nightly/integration.cfg new file mode 100644 index 0000000000..3b017fc80f --- /dev/null +++ b/.kokoro/nightly/integration.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} diff --git a/.kokoro/nightly/java11.cfg b/.kokoro/nightly/java11.cfg new file mode 100644 index 0000000000..709f2b4c73 --- /dev/null +++ b/.kokoro/nightly/java11.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java11" +} diff --git a/.kokoro/nightly/java7.cfg b/.kokoro/nightly/java7.cfg new file mode 100644 index 0000000000..cb24f44eea --- /dev/null +++ b/.kokoro/nightly/java7.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java7" +} diff --git a/.kokoro/nightly/java8-osx.cfg b/.kokoro/nightly/java8-osx.cfg new file mode 100644 index 0000000000..8b617e2594 --- /dev/null +++ b/.kokoro/nightly/java8-osx.cfg @@ -0,0 +1,3 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +build_file: "java-core/.kokoro/build.sh" diff --git a/.kokoro/nightly/java8-win.cfg b/.kokoro/nightly/java8-win.cfg new file mode 100644 index 0000000000..1a6311a420 --- /dev/null +++ b/.kokoro/nightly/java8-win.cfg @@ -0,0 +1,3 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +build_file: "java-core/.kokoro/build.bat" diff --git a/.kokoro/nightly/java8.cfg b/.kokoro/nightly/java8.cfg new file mode 100644 index 0000000000..3b017fc80f --- /dev/null +++ b/.kokoro/nightly/java8.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} diff --git a/.kokoro/nightly/lint.cfg b/.kokoro/nightly/lint.cfg new file mode 100644 index 0000000000..6d323c8ae7 --- /dev/null +++ b/.kokoro/nightly/lint.cfg @@ -0,0 +1,13 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. + +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} + +env_vars: { + key: "JOB_TYPE" + value: "lint" +} \ No newline at end of file diff --git a/.kokoro/presubmit/common.cfg b/.kokoro/presubmit/common.cfg new file mode 100644 index 0000000000..4ba4e252c5 --- /dev/null +++ b/.kokoro/presubmit/common.cfg @@ -0,0 +1,33 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "java-core/.kokoro/trampoline.sh" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/build.sh" +} + +env_vars: { + key: "JOB_TYPE" + value: "test" +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "dpebot_codecov_token" + } + } +} diff --git a/.kokoro/presubmit/dependencies.cfg b/.kokoro/presubmit/dependencies.cfg new file mode 100644 index 0000000000..895832a928 --- /dev/null +++ b/.kokoro/presubmit/dependencies.cfg @@ -0,0 +1,12 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/dependencies.sh" +} diff --git a/.kokoro/presubmit/integration.cfg b/.kokoro/presubmit/integration.cfg new file mode 100644 index 0000000000..141f90c13c --- /dev/null +++ b/.kokoro/presubmit/integration.cfg @@ -0,0 +1,31 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} + +env_vars: { + key: "JOB_TYPE" + value: "integration" +} + +env_vars: { + key: "GCLOUD_PROJECT" + value: "gcloud-devel" +} + +env_vars: { + key: "GOOGLE_APPLICATION_CREDENTIALS" + value: "keystore/73713_java_it_service_account" +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "java_it_service_account" + } + } +} diff --git a/.kokoro/presubmit/java11.cfg b/.kokoro/presubmit/java11.cfg new file mode 100644 index 0000000000..709f2b4c73 --- /dev/null +++ b/.kokoro/presubmit/java11.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java11" +} diff --git a/.kokoro/presubmit/java7.cfg b/.kokoro/presubmit/java7.cfg new file mode 100644 index 0000000000..cb24f44eea --- /dev/null +++ b/.kokoro/presubmit/java7.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java7" +} diff --git a/.kokoro/presubmit/java8-osx.cfg b/.kokoro/presubmit/java8-osx.cfg new file mode 100644 index 0000000000..8b617e2594 --- /dev/null +++ b/.kokoro/presubmit/java8-osx.cfg @@ -0,0 +1,3 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +build_file: "java-core/.kokoro/build.sh" diff --git a/.kokoro/presubmit/java8-win.cfg b/.kokoro/presubmit/java8-win.cfg new file mode 100644 index 0000000000..1a6311a420 --- /dev/null +++ b/.kokoro/presubmit/java8-win.cfg @@ -0,0 +1,3 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +build_file: "java-core/.kokoro/build.bat" diff --git a/.kokoro/presubmit/java8.cfg b/.kokoro/presubmit/java8.cfg new file mode 100644 index 0000000000..3b017fc80f --- /dev/null +++ b/.kokoro/presubmit/java8.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} diff --git a/.kokoro/presubmit/linkage-monitor.cfg b/.kokoro/presubmit/linkage-monitor.cfg new file mode 100644 index 0000000000..8703ea2a3e --- /dev/null +++ b/.kokoro/presubmit/linkage-monitor.cfg @@ -0,0 +1,12 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/linkage-monitor.sh" +} \ No newline at end of file diff --git a/.kokoro/presubmit/lint.cfg b/.kokoro/presubmit/lint.cfg new file mode 100644 index 0000000000..6d323c8ae7 --- /dev/null +++ b/.kokoro/presubmit/lint.cfg @@ -0,0 +1,13 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. + +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} + +env_vars: { + key: "JOB_TYPE" + value: "lint" +} \ No newline at end of file diff --git a/.kokoro/release/bump_snapshot.cfg b/.kokoro/release/bump_snapshot.cfg new file mode 100644 index 0000000000..a27810aacf --- /dev/null +++ b/.kokoro/release/bump_snapshot.cfg @@ -0,0 +1,53 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "java-core/.kokoro/trampoline.sh" + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/release/bump_snapshot.sh" +} + +# tokens used by release-please to keep an up-to-date release PR. +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "github-magic-proxy-key-release-please" + } + } +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "github-magic-proxy-token-release-please" + } + } +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "github-magic-proxy-url-release-please" + } + } +} diff --git a/.kokoro/release/bump_snapshot.sh b/.kokoro/release/bump_snapshot.sh new file mode 100755 index 0000000000..712ce36594 --- /dev/null +++ b/.kokoro/release/bump_snapshot.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Copyright 2019 Google LLC +# +# 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 +# +# https://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. + +set -eo pipefail + +export NPM_CONFIG_PREFIX=/home/node/.npm-global + +if [ -f ${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-url-release-please ]; then + # Groom the snapshot release PR immediately after publishing a release + npx release-please release-pr --token=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-token-release-please \ + --repo-url=googleapis/java-core \ + --package-name="google-cloud-core" \ + --api-url=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-url-release-please \ + --proxy-key=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-key-release-please \ + --snapshot \ + --release-type=java-auth-yoshi +fi diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg new file mode 100644 index 0000000000..4b5b862167 --- /dev/null +++ b/.kokoro/release/common.cfg @@ -0,0 +1,49 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "java-core/.kokoro/trampoline.sh" + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 70247 + keyname: "maven-gpg-keyring" + } + } +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 70247 + keyname: "maven-gpg-passphrase" + } + } +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 70247 + keyname: "maven-gpg-pubkeyring" + } + } +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 70247 + keyname: "sonatype-credentials" + } + } +} diff --git a/.kokoro/release/common.sh b/.kokoro/release/common.sh new file mode 100755 index 0000000000..6e3f65999b --- /dev/null +++ b/.kokoro/release/common.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Copyright 2018 Google Inc. +# +# 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. + +set -eo pipefail + +# Get secrets from keystore and set and environment variables +setup_environment_secrets() { + export GPG_PASSPHRASE=$(cat ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-passphrase) + export GPG_TTY=$(tty) + export GPG_HOMEDIR=/gpg + mkdir $GPG_HOMEDIR + mv ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-pubkeyring $GPG_HOMEDIR/pubring.gpg + mv ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-keyring $GPG_HOMEDIR/secring.gpg + export SONATYPE_USERNAME=$(cat ${KOKORO_KEYSTORE_DIR}/70247_sonatype-credentials | cut -f1 -d'|') + export SONATYPE_PASSWORD=$(cat ${KOKORO_KEYSTORE_DIR}/70247_sonatype-credentials | cut -f2 -d'|') +} + +create_settings_xml_file() { + echo " + + + ossrh + ${SONATYPE_USERNAME} + ${SONATYPE_PASSWORD} + + + sonatype-nexus-staging + ${SONATYPE_USERNAME} + ${SONATYPE_PASSWORD} + + + sonatype-nexus-snapshots + ${SONATYPE_USERNAME} + ${SONATYPE_PASSWORD} + + +" > $1 +} \ No newline at end of file diff --git a/.kokoro/release/drop.cfg b/.kokoro/release/drop.cfg new file mode 100644 index 0000000000..211c014c2a --- /dev/null +++ b/.kokoro/release/drop.cfg @@ -0,0 +1,9 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/release/drop.sh" +} + +# Download staging properties file. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/java/releases/java-core" \ No newline at end of file diff --git a/.kokoro/release/drop.sh b/.kokoro/release/drop.sh new file mode 100755 index 0000000000..5c4551efa2 --- /dev/null +++ b/.kokoro/release/drop.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Copyright 2018 Google Inc. +# +# 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. + +set -eo pipefail + +# STAGING_REPOSITORY_ID must be set +if [ -z "${STAGING_REPOSITORY_ID}" ]; then + echo "Missing STAGING_REPOSITORY_ID environment variable" + exit 1 +fi + +source $(dirname "$0")/common.sh +pushd $(dirname "$0")/../../ + +setup_environment_secrets +create_settings_xml_file "settings.xml" + +mvn nexus-staging:drop -B \ + --settings=settings.xml \ + -DstagingRepositoryId=${STAGING_REPOSITORY_ID} diff --git a/.kokoro/release/promote.cfg b/.kokoro/release/promote.cfg new file mode 100644 index 0000000000..b3fc2015d6 --- /dev/null +++ b/.kokoro/release/promote.cfg @@ -0,0 +1,10 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/release/promote.sh" +} + +# Download staging properties file. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/java/releases/java-core" + diff --git a/.kokoro/release/promote.sh b/.kokoro/release/promote.sh new file mode 100755 index 0000000000..1fa95fa537 --- /dev/null +++ b/.kokoro/release/promote.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Copyright 2018 Google Inc. +# +# 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. + +set -eo pipefail + +# STAGING_REPOSITORY_ID must be set +if [ -z "${STAGING_REPOSITORY_ID}" ]; then + echo "Missing STAGING_REPOSITORY_ID environment variable" + exit 1 +fi + +source $(dirname "$0")/common.sh + +pushd $(dirname "$0")/../../ + +setup_environment_secrets +create_settings_xml_file "settings.xml" + +mvn nexus-staging:release -B \ + -DperformRelease=true \ + --settings=settings.xml \ + -DstagingRepositoryId=${STAGING_REPOSITORY_ID} diff --git a/.kokoro/release/publish_javadoc.cfg b/.kokoro/release/publish_javadoc.cfg new file mode 100644 index 0000000000..166868b690 --- /dev/null +++ b/.kokoro/release/publish_javadoc.cfg @@ -0,0 +1,19 @@ +# Format: //devtools/kokoro/config/proto/build.proto +env_vars: { + key: "STAGING_BUCKET" + value: "docs-staging" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/release/publish_javadoc.sh" +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "docuploader_service_account" + } + } +} diff --git a/.kokoro/release/publish_javadoc.sh b/.kokoro/release/publish_javadoc.sh new file mode 100755 index 0000000000..3eb587a7dc --- /dev/null +++ b/.kokoro/release/publish_javadoc.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Copyright 2019 Google Inc. +# +# 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. + +set -eo pipefail + +if [[ -z "${CREDENTIALS}" ]]; then + CREDENTIALS=${KOKORO_KEYSTORE_DIR}/73713_docuploader_service_account +fi + +if [[ -z "${STAGING_BUCKET}" ]]; then + echo "Need to set STAGING_BUCKET environment variable" + exit 1 +fi + +# work from the git root directory +pushd $(dirname "$0")/../../ + +# install docuploader package +python3 -m pip install gcp-docuploader + +# compile all packages +mvn clean install -B -DskipTests=true + +NAME=google-cloud-core +VERSION=$(grep ${NAME}: versions.txt | cut -d: -f3) + +# build the docs +mvn site -B + +pushd target/site/apidocs + +# create metadata +python3 -m docuploader create-metadata \ + --name ${NAME} \ + --version ${VERSION} \ + --language java + +# upload docs +python3 -m docuploader upload . \ + --credentials ${CREDENTIALS} \ + --staging-bucket ${STAGING_BUCKET} + +popd diff --git a/.kokoro/release/snapshot.cfg b/.kokoro/release/snapshot.cfg new file mode 100644 index 0000000000..d435702932 --- /dev/null +++ b/.kokoro/release/snapshot.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/release/snapshot.sh" +} \ No newline at end of file diff --git a/.kokoro/release/snapshot.sh b/.kokoro/release/snapshot.sh new file mode 100755 index 0000000000..bf738c56dd --- /dev/null +++ b/.kokoro/release/snapshot.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Copyright 2019 Google LLC +# +# 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. + +set -eo pipefail + +source $(dirname "$0")/common.sh +MAVEN_SETTINGS_FILE=$(realpath $(dirname "$0")/../../)/settings.xml +pushd $(dirname "$0")/../../ + +setup_environment_secrets +create_settings_xml_file "settings.xml" + +mvn clean install deploy -B \ + --settings ${MAVEN_SETTINGS_FILE} \ + -DperformRelease=true \ + -Dgpg.executable=gpg \ + -Dgpg.passphrase=${GPG_PASSPHRASE} \ + -Dgpg.homedir=${GPG_HOMEDIR} \ No newline at end of file diff --git a/.kokoro/release/stage.cfg b/.kokoro/release/stage.cfg new file mode 100644 index 0000000000..7d8a196382 --- /dev/null +++ b/.kokoro/release/stage.cfg @@ -0,0 +1,44 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/java-core/.kokoro/release/stage.sh" +} + +# Need to save the properties file +action { + define_artifacts { + regex: "github/java-core/target/nexus-staging/staging/*.properties" + strip_prefix: "github/java-core" + } +} + +# Fetch the token needed for reporting release status to GitHub +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "yoshi-automation-github-key" + } + } +} + +# Fetch magictoken to use with Magic Github Proxy +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "releasetool-magictoken" + } + } +} + +# Fetch api key to use with Magic Github Proxy +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "magic-github-proxy-api-key" + } + } +} diff --git a/.kokoro/release/stage.sh b/.kokoro/release/stage.sh new file mode 100755 index 0000000000..b1b1b01c66 --- /dev/null +++ b/.kokoro/release/stage.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Copyright 2018 Google Inc. +# +# 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. + +set -eo pipefail + +# Start the releasetool reporter +python3 -m pip install gcp-releasetool +python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script + +source $(dirname "$0")/common.sh +MAVEN_SETTINGS_FILE=$(realpath $(dirname "$0")/../../)/settings.xml +pushd $(dirname "$0")/../../ + +setup_environment_secrets +create_settings_xml_file "settings.xml" + +mvn clean install deploy -B \ + --settings ${MAVEN_SETTINGS_FILE} \ + -DperformRelease=true \ + -Dgpg.executable=gpg \ + -Dgpg.passphrase=${GPG_PASSPHRASE} \ + -Dgpg.homedir=${GPG_HOMEDIR} + +if [[ -n "${AUTORELEASE_PR}" ]] +then + mvn nexus-staging:release -B \ + -DperformRelease=true \ + --settings=settings.xml +fi \ No newline at end of file diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh new file mode 100644 index 0000000000..ba17ce0146 --- /dev/null +++ b/.kokoro/trampoline.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Copyright 2018 Google Inc. +# +# 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. +set -eo pipefail +# Always run the cleanup script, regardless of the success of bouncing into +# the container. +function cleanup() { + chmod +x ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh + ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh + echo "cleanup"; +} +trap cleanup EXIT +python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" diff --git a/.repo-metadata.json b/.repo-metadata.json new file mode 100644 index 0000000000..e3e884930d --- /dev/null +++ b/.repo-metadata.json @@ -0,0 +1,9 @@ +{ + "name": "google-cloud-core", + "name_pretty": "Google Cloud Core", + "release_level": "ga", + "language": "java", + "repo": "googleapis/java-core", + "repo_short": "java-core", + "distribution_name": "google-cloud-java" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..d622ee88bd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +## [1.91.0](https://www.github.com/googleapis/java-core/compare/v1.90.0...v1.91.0) (2019-09-18) + + +### Dependencies + +* update dependency com.google.api-client:google-api-client-bom to v1.30.3 ([#21](https://www.github.com/googleapis/java-core/issues/21)) ([fcd67f8](https://www.github.com/googleapis/java-core/commit/fcd67f8)) +* update opencensus packages to v0.24.0 ([#22](https://www.github.com/googleapis/java-core/issues/22)) ([4b21afa](https://www.github.com/googleapis/java-core/commit/4b21afa)) + + +### Documentation + +* fix Kokoro badge link ([19d79d6](https://www.github.com/googleapis/java-core/commit/19d79d6)) +* fix README versions and CI Status table ([6e3ccf3](https://www.github.com/googleapis/java-core/commit/6e3ccf3)) +* update README with a better project description ([#17](https://www.github.com/googleapis/java-core/issues/17)) ([018d4d5](https://www.github.com/googleapis/java-core/commit/018d4d5)) + + +### Features + +* add google-cloud-core-bom artifact ([#13](https://www.github.com/googleapis/java-core/issues/13)) ([3cb19a0](https://www.github.com/googleapis/java-core/commit/3cb19a0)) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..6b2238bb75 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,93 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when the Project +Steward has a reasonable belief that an individual's behavior may have a +negative impact on the project or its community. + +## Conflict Resolution + +We do not believe that all conflict is bad; healthy debate and disagreement +often yield positive results. However, it is never okay to be disrespectful or +to engage in behavior that violates the project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address +the behavior directly with those involved. Many issues can be resolved quickly +and easily, and this gives people more control over the outcome of their +dispute. If you are unable to resolve the matter for any reason, or if the +behavior is threatening or harassing, report it. We are dedicated to providing +an environment where participants feel welcome and safe. + +Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the +Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to +receive and address reported violations of the code of conduct. They will then +work with a committee consisting of representatives from the Open Source +Programs Office and the Google Open Source Strategy team. If for any reason you +are uncomfortable reaching out the Project Steward, please email +opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is taken. +The identity of the reporter will be omitted from the details of the report +supplied to the accused. In potentially harmful situations, such as ongoing +harassment or threats to anyone's safety, we may take action without notice. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..ebbb59e531 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). \ No newline at end of file diff --git a/LICENSE b/LICENSE index 4eedc0116a..d645695673 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ -Apache License + + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -178,7 +179,7 @@ Apache License 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 "{}" + 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000000..c1ca94e02c --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Google Cloud Java Client -- Core + +A set of classes and utilities used in Google Cloud Java libraries. + +*Note*: This library is only meant to be consumed by other Google Libraries. + +[![Maven](https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-core.svg)](https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-core.svg) + +- [API Documentation][api-docs] + +## Java Versions + +Java 7 or above is required for using this client. + +## Contributing + +Contributions to this library are always welcome and highly encouraged. + +See [CONTRIBUTING][contributing] documentation for more information on how to get started. + +Please note that this project is released with a Contributor Code of Conduct. By participating in +this project you agree to abide by its terms. See [Code of Conduct][code-of-conduct] for more +information. + +## Versioning + +This library follows [Semantic Versioning][semver]. + +It is currently in major version one (``1.y.z``), which means that the public API should be +considered stable. + +## License + +Apache 2.0 - See [LICENSE][license] for more information. + +## CI Status + +Java Version | Status +------------ | ------ +Java 7 | [![Kokoro CI](https://storage.googleapis.com/cloud-devrel-public/java/badges/java-core/java7.svg)](https://storage.googleapis.com/cloud-devrel-public/java/badges/java-core/java7.html) +Java 8 | [![Kokoro CI](https://storage.googleapis.com/cloud-devrel-public/java/badges/java-core/java8.svg)](https://storage.googleapis.com/cloud-devrel-public/java/badges/java-core/java8.html) +Java 8 OSX | [![Kokoro CI](https://storage.googleapis.com/cloud-devrel-public/java/badges/java-core/java8-osx.svg)](https://storage.googleapis.com/cloud-devrel-public/java/badges/java-core/java8-osx.html) +Java 8 Windows | [![Kokoro CI](https://storage.googleapis.com/cloud-devrel-public/java/badges/java-core/java8-win.svg)](https://storage.googleapis.com/cloud-devrel-public/java/badges/java-core/java8-win.html) +Java 11 | [![Kokoro CI](https://storage.googleapis.com/cloud-devrel-public/java/badges/java-core/java11.svg)](https://storage.googleapis.com/cloud-devrel-public/java/badges/java-core/java11.html) + + +[contributing]: https://github.com/googleapis/java-core/blob/master/CONTRIBUTING.md +[code-of-conduct]: https://github.com/googleapis/java-core/blob/master/CODE_OF_CONDUCT.md +[license]: https://github.com/googleapis/java-core/blob/master/LICENSE +[semver]: http://semver.org/ +[cloud-platform]: https://cloud.google.com/ +[api-docs]: https://googleapis.dev/java/google-cloud-core/latest diff --git a/codecov.yaml b/codecov.yaml new file mode 100644 index 0000000000..5724ea9478 --- /dev/null +++ b/codecov.yaml @@ -0,0 +1,4 @@ +--- +codecov: + ci: + - source.cloud.google.com diff --git a/google-cloud-core-bom/pom.xml b/google-cloud-core-bom/pom.xml new file mode 100644 index 0000000000..b5514736f7 --- /dev/null +++ b/google-cloud-core-bom/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + com.google.cloud + google-cloud-core-bom + 1.91.0 + pom + + com.google.cloud + google-cloud-shared-config + 0.1.2 + + + Google Cloud Core + https://github.com/googleapis/java-core/tree/master/google-cloud-core-bom + + BOM for Google Cloud Core + + + + Google LLC + + + + + chingor13 + Jeff Ching + chingor@google.com + Google LLC + + Developer + + + + + + scm:git:https://github.com/googleapis/java-core.git + scm:git:git@github.com:googleapis/java-core.git + https://github.com/googleapis/java-core + + + + + sonatype-nexus-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + + sonatype-nexus-staging + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + + com.google.cloud + google-cloud-core + 1.91.0 + + + com.google.cloud + google-cloud-core-grpc + 1.91.0 + + + com.google.cloud + google-cloud-core-http + 1.91.0 + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + true + + + + + diff --git a/google-cloud-core-grpc/pom.xml b/google-cloud-core-grpc/pom.xml new file mode 100644 index 0000000000..c592d4720c --- /dev/null +++ b/google-cloud-core-grpc/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + com.google.cloud + google-cloud-core-grpc + 1.91.0 + jar + Google Cloud Core gRPC + https://github.com/googleapis/google-cloud-java/tree/master/google-cloud-clients/google-cloud-core-grpc + + Core gRPC module for the google-cloud. + + + com.google.cloud + google-cloud-core-parent + 1.91.0 + + + google-cloud-core-grpc + + + + com.google.auth + google-auth-library-credentials + + + com.google.cloud + google-cloud-core + + + com.google.guava + guava + + + com.google.api + gax + + + com.google.api + gax-grpc + + + com.google.api + api-common + + + io.grpc + grpc-api + + + io.grpc + grpc-core + + + com.google.http-client + google-http-client + + + + junit + junit + test + + + org.easymock + easymock + test + + + org.objenesis + objenesis + test + + + \ No newline at end of file diff --git a/google-cloud-core-grpc/src/main/java/com/google/cloud/grpc/BaseGrpcServiceException.java b/google-cloud-core-grpc/src/main/java/com/google/cloud/grpc/BaseGrpcServiceException.java new file mode 100644 index 0000000000..dffa4d37ee --- /dev/null +++ b/google-cloud-core-grpc/src/main/java/com/google/cloud/grpc/BaseGrpcServiceException.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.grpc; + +import com.google.api.client.http.HttpResponseException; +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.api.gax.rpc.ApiException; +import com.google.cloud.BaseServiceException; +import com.google.common.base.MoreObjects; +import java.io.IOException; +import java.util.Collections; + +/** Base class for all exceptions from grpc-based services. */ +public class BaseGrpcServiceException extends BaseServiceException { + + private static final long serialVersionUID = -2685197215731335549L; + + @InternalApi("This class should only be extended within google-cloud-java") + protected BaseGrpcServiceException(String message, Throwable cause, int code, boolean retryable) { + super( + ExceptionData.newBuilder() + .setMessage(message) + .setCause(cause) + .setCode(code) + .setRetryable(retryable) + .build()); + } + + @InternalApi("This class should only be extended within google-cloud-java") + protected BaseGrpcServiceException(IOException exception, boolean idempotent) { + super(makeExceptionData(exception, idempotent)); + } + + private static ExceptionData makeExceptionData(IOException exception, boolean idempotent) { + int code = UNKNOWN_CODE; + Boolean retryable = null; + if (exception instanceof HttpResponseException) { + // In cases where an exception is an instance of HttpResponseException, + // check the status code to determine whether it's retryable + code = ((HttpResponseException) exception).getStatusCode(); + retryable = + BaseServiceException.isRetryable(code, null, idempotent, Collections.emptySet()); + } + return ExceptionData.newBuilder() + .setMessage(exception.getMessage()) + .setCause(exception) + .setRetryable( + MoreObjects.firstNonNull( + retryable, BaseServiceException.isRetryable(idempotent, exception))) + .setCode(code) + .setReason(null) + .setLocation(null) + .setDebugInfo(null) + .build(); + } + + @BetaApi + public BaseGrpcServiceException(ApiException apiException) { + super( + ExceptionData.newBuilder() + .setMessage(apiException.getMessage()) + .setCause(apiException) + .setRetryable(apiException.isRetryable()) + .setCode(apiException.getStatusCode().getCode().getHttpStatusCode()) + .setReason(apiException.getStatusCode().getCode().name()) + .setLocation(null) + .setDebugInfo(null) + .build()); + } +} diff --git a/google-cloud-core-grpc/src/main/java/com/google/cloud/grpc/GrpcTransportOptions.java b/google-cloud-core-grpc/src/main/java/com/google/cloud/grpc/GrpcTransportOptions.java new file mode 100644 index 0000000000..841cc8218b --- /dev/null +++ b/google-cloud-core-grpc/src/main/java/com/google/cloud/grpc/GrpcTransportOptions.java @@ -0,0 +1,208 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.grpc; + +import static com.google.common.base.MoreObjects.firstNonNull; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.api.gax.core.CredentialsProvider; +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.rpc.TransportChannelProvider; +import com.google.api.gax.rpc.UnaryCallSettings; +import com.google.auth.Credentials; +import com.google.cloud.NoCredentials; +import com.google.cloud.ServiceOptions; +import com.google.cloud.TransportOptions; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.grpc.internal.SharedResourceHolder; +import io.grpc.internal.SharedResourceHolder.Resource; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** Class representing service options for those services that use gRPC as the transport layer. */ +public class GrpcTransportOptions implements TransportOptions { + + private static final long serialVersionUID = -9049538465533951165L; + private final String executorFactoryClassName; + + private transient ExecutorFactory executorFactory; + + /** Shared thread pool executor. */ + private static final Resource EXECUTOR = + new Resource() { + @Override + public ScheduledExecutorService create() { + ScheduledThreadPoolExecutor service = + new ScheduledThreadPoolExecutor( + 8, + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("grpc-transport-%d") + .build()); + service.setKeepAliveTime(5, TimeUnit.SECONDS); + service.allowCoreThreadTimeOut(true); + service.setRemoveOnCancelPolicy(true); + return service; + } + + @Override + public void close(ScheduledExecutorService instance) { + instance.shutdown(); + } + }; + + /** + * An interface for {@link ExecutorService} factories. Implementations of this interface can be + * used to provide an user-defined executor to execute requests. Any implementation of this + * interface must override the {@code get()} method to return the desired executor. The {@code + * release(executor)} method should be overriden to free resources used by the executor (if + * needed) according to application's logic. + * + *

Implementation must provide a public no-arg constructor. Loading of a factory implementation + * is done via {@link java.util.ServiceLoader}. + * + * @param the {@link ExecutorService} subclass created by this factory + */ + public interface ExecutorFactory { + + /** Gets an executor service instance. */ + T get(); + + /** Releases resources used by the executor and possibly shuts it down. */ + void release(T executor); + } + + @InternalApi + public static class DefaultExecutorFactory implements ExecutorFactory { + + private static final DefaultExecutorFactory INSTANCE = new DefaultExecutorFactory(); + + @Override + public ScheduledExecutorService get() { + return SharedResourceHolder.get(EXECUTOR); + } + + @Override + public synchronized void release(ScheduledExecutorService executor) { + SharedResourceHolder.release(EXECUTOR, executor); + } + } + + /** Builder for {@code GrpcTransportOptions}. */ + public static class Builder { + + private ExecutorFactory executorFactory; + + private Builder() {} + + private Builder(GrpcTransportOptions options) { + executorFactory = options.executorFactory; + } + + public GrpcTransportOptions build() { + return new GrpcTransportOptions(this); + } + + /** + * Sets the scheduled executor factory. This method can be used to provide an user-defined + * scheduled executor to execute requests. + * + * @return the builder + */ + public Builder setExecutorFactory(ExecutorFactory executorFactory) { + this.executorFactory = executorFactory; + return this; + } + } + + @SuppressWarnings("unchecked") + private GrpcTransportOptions(Builder builder) { + executorFactory = + firstNonNull( + builder.executorFactory, + ServiceOptions.getFromServiceLoader( + ExecutorFactory.class, DefaultExecutorFactory.INSTANCE)); + executorFactoryClassName = executorFactory.getClass().getName(); + } + + /** Returns a scheduled executor service provider. */ + public ExecutorFactory getExecutorFactory() { + return executorFactory; + } + + /** Returns a builder for API call settings. */ + @Deprecated + public UnaryCallSettings.Builder getApiCallSettings(RetrySettings retrySettings) { + return UnaryCallSettings.newUnaryCallSettingsBuilder().setRetrySettings(retrySettings); + } + + /** Returns a channel provider from the given default provider. */ + @BetaApi + public static TransportChannelProvider setUpChannelProvider( + InstantiatingGrpcChannelProvider.Builder providerBuilder, + ServiceOptions serviceOptions) { + providerBuilder.setEndpoint(serviceOptions.getHost()); + return providerBuilder.build(); + } + + public static CredentialsProvider setUpCredentialsProvider(ServiceOptions serviceOptions) { + Credentials scopedCredentials = serviceOptions.getScopedCredentials(); + if (scopedCredentials != null && scopedCredentials != NoCredentials.getInstance()) { + return FixedCredentialsProvider.create(scopedCredentials); + } + return NoCredentialsProvider.create(); + } + + public Builder toBuilder() { + return new Builder(this); + } + + @Override + public int hashCode() { + return Objects.hash(executorFactoryClassName); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + GrpcTransportOptions other = (GrpcTransportOptions) obj; + return Objects.equals(executorFactoryClassName, other.executorFactoryClassName); + } + + private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { + input.defaultReadObject(); + executorFactory = ServiceOptions.newInstance(executorFactoryClassName); + } + + public static Builder newBuilder() { + return new Builder(); + } +} diff --git a/google-cloud-core-grpc/src/test/java/com/google/cloud/grpc/BaseGrpcServiceExceptionTest.java b/google-cloud-core-grpc/src/test/java/com/google/cloud/grpc/BaseGrpcServiceExceptionTest.java new file mode 100644 index 0000000000..696de60c55 --- /dev/null +++ b/google-cloud-core-grpc/src/test/java/com/google/cloud/grpc/BaseGrpcServiceExceptionTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.grpc; + +import static org.easymock.EasyMock.createMock; +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.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.api.gax.grpc.GrpcStatusCode; +import com.google.api.gax.rpc.InternalException; +import com.google.cloud.BaseServiceException; +import com.google.cloud.RetryHelper; +import io.grpc.Status.Code; +import java.io.IOException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import org.junit.Test; + +public class BaseGrpcServiceExceptionTest { + + private static final String MESSAGE = "some message"; + private static final boolean NOT_RETRYABLE = false; + private static final boolean IDEMPOTENT = true; + + @Test + public void testBaseServiceException() { + BaseGrpcServiceException serviceException = null; + + IOException exception = new SocketTimeoutException(); + serviceException = new BaseGrpcServiceException(exception, IDEMPOTENT); + assertTrue(serviceException.isRetryable()); + assertNull(serviceException.getMessage()); + assertEquals(exception, serviceException.getCause()); + assertNull(serviceException.getReason()); + assertNull(serviceException.getLocation()); + assertNull(serviceException.getDebugInfo()); + + exception = new SocketException(); + serviceException = new BaseGrpcServiceException(exception, IDEMPOTENT); + assertTrue(serviceException.isRetryable()); + assertNull(serviceException.getMessage()); + assertEquals(exception, serviceException.getCause()); + assertNull(serviceException.getReason()); + assertNull(serviceException.getLocation()); + assertNull(serviceException.getDebugInfo()); + + exception = new IOException("insufficient data written"); + serviceException = new BaseGrpcServiceException(exception, IDEMPOTENT); + assertTrue(serviceException.isRetryable()); + assertEquals("insufficient data written", serviceException.getMessage()); + assertEquals(exception, serviceException.getCause()); + assertNull(serviceException.getReason()); + assertNull(serviceException.getLocation()); + assertNull(serviceException.getDebugInfo()); + + Exception cause = new IllegalArgumentException("bad arg"); + InternalException apiException = + new InternalException(MESSAGE, cause, GrpcStatusCode.of(Code.INTERNAL), NOT_RETRYABLE); + serviceException = new BaseGrpcServiceException(apiException); + assertFalse(serviceException.isRetryable()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(apiException, serviceException.getCause()); + assertEquals(500, serviceException.getCode()); + assertEquals(Code.INTERNAL.name(), serviceException.getReason()); + assertNull(serviceException.getLocation()); + assertNull(serviceException.getDebugInfo()); + } + + @Test + public void testTranslateAndThrow() throws Exception { + IOException exception = new SocketTimeoutException(); + BaseGrpcServiceException cause = new BaseGrpcServiceException(exception, IDEMPOTENT); + RetryHelper.RetryHelperException exceptionMock = + createMock(RetryHelper.RetryHelperException.class); + expect(exceptionMock.getCause()).andReturn(cause).times(2); + replay(exceptionMock); + try { + BaseServiceException.translate(exceptionMock); + } catch (BaseServiceException ex) { + assertEquals(0, ex.getCode()); + assertNull(ex.getMessage()); + assertTrue(ex.isRetryable()); + } finally { + verify(exceptionMock); + } + } +} diff --git a/google-cloud-core-grpc/src/test/java/com/google/cloud/grpc/GrpcTransportOptionsTest.java b/google-cloud-core-grpc/src/test/java/com/google/cloud/grpc/GrpcTransportOptionsTest.java new file mode 100644 index 0000000000..b51eab96ce --- /dev/null +++ b/google-cloud-core-grpc/src/test/java/com/google/cloud/grpc/GrpcTransportOptionsTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.grpc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.grpc.GrpcTransportOptions.DefaultExecutorFactory; +import com.google.cloud.grpc.GrpcTransportOptions.ExecutorFactory; +import java.util.concurrent.ScheduledExecutorService; +import org.easymock.EasyMock; +import org.junit.Test; + +public class GrpcTransportOptionsTest { + + private static final ExecutorFactory MOCK_EXECUTOR_FACTORY = + EasyMock.createMock(ExecutorFactory.class); + private static final GrpcTransportOptions OPTIONS = + GrpcTransportOptions.newBuilder().setExecutorFactory(MOCK_EXECUTOR_FACTORY).build(); + private static final GrpcTransportOptions DEFAULT_OPTIONS = + GrpcTransportOptions.newBuilder().build(); + private static final GrpcTransportOptions OPTIONS_COPY = OPTIONS.toBuilder().build(); + + @Test + public void testBuilder() { + assertSame(MOCK_EXECUTOR_FACTORY, OPTIONS.getExecutorFactory()); + assertTrue(DEFAULT_OPTIONS.getExecutorFactory() instanceof DefaultExecutorFactory); + } + + @Test + public void testBaseEquals() { + assertEquals(OPTIONS, OPTIONS_COPY); + assertNotEquals(DEFAULT_OPTIONS, OPTIONS); + GrpcTransportOptions options = + OPTIONS.toBuilder().setExecutorFactory(new DefaultExecutorFactory()).build(); + assertNotEquals(OPTIONS, options); + } + + @Test + public void testBaseHashCode() { + assertEquals(OPTIONS.hashCode(), OPTIONS_COPY.hashCode()); + assertNotEquals(DEFAULT_OPTIONS.hashCode(), OPTIONS.hashCode()); + GrpcTransportOptions options = + OPTIONS.toBuilder().setExecutorFactory(new DefaultExecutorFactory()).build(); + assertNotEquals(OPTIONS.hashCode(), options.hashCode()); + } + + @Test + public void testDefaultExecutorFactory() { + ExecutorFactory executorFactory = new DefaultExecutorFactory(); + ScheduledExecutorService executorService = executorFactory.get(); + assertSame(executorService, executorFactory.get()); + } +} diff --git a/google-cloud-core-http/pom.xml b/google-cloud-core-http/pom.xml new file mode 100644 index 0000000000..9740de7202 --- /dev/null +++ b/google-cloud-core-http/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + com.google.cloud + google-cloud-core-http + 1.91.0 + jar + Google Cloud Core HTTP + https://github.com/googleapis/google-cloud-java/tree/master/google-cloud-clients/google-cloud-core-http + + Core http module for the google-cloud. + + + com.google.cloud + google-cloud-core-parent + 1.91.0 + + + google-cloud-core-http + + + + com.google.cloud + google-cloud-core + + + com.google.auth + google-auth-library-credentials + + + com.google.auth + google-auth-library-oauth2-http + + + com.google.http-client + google-http-client + + + com.google.guava + guava + + + com.google.api-client + google-api-client + + + com.google.http-client + google-http-client-appengine + + + com.google.api + gax + + + com.google.api + gax-httpjson + + + com.google.code.findbugs + jsr305 + + + io.opencensus + opencensus-api + + + io.opencensus + opencensus-contrib-http-util + + + com.google.api + api-common + + + + + junit + junit + test + + + org.easymock + easymock + test + + + org.objenesis + objenesis + test + + + com.google.truth + truth + test + + + diff --git a/google-cloud-core-http/src/main/java/com/google/cloud/http/BaseHttpServiceException.java b/google-cloud-core-http/src/main/java/com/google/cloud/http/BaseHttpServiceException.java new file mode 100644 index 0000000000..26f43b276f --- /dev/null +++ b/google-cloud-core-http/src/main/java/com/google/cloud/http/BaseHttpServiceException.java @@ -0,0 +1,166 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.http; + +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.HttpResponseException; +import com.google.api.core.InternalApi; +import com.google.cloud.BaseServiceException; +import com.google.common.base.MoreObjects; +import java.io.IOException; +import java.util.Set; + +/** Base class for all exceptions from http-based services. */ +public class BaseHttpServiceException extends BaseServiceException { + + private static final long serialVersionUID = -5793034110344127954L; + public static final int UNKNOWN_CODE = 0; + + @InternalApi("This class should only be extended within google-cloud-java") + protected BaseHttpServiceException( + IOException exception, boolean idempotent, Set retryableErrors) { + super(makeExceptionData(exception, idempotent, retryableErrors)); + } + + private static ExceptionData makeExceptionData( + IOException exception, boolean idempotent, Set retryableErrors) { + int code = UNKNOWN_CODE; + String reason = null; + String location = null; + String debugInfo = null; + Boolean retryable = null; + if (exception instanceof HttpResponseException) { + if (exception instanceof GoogleJsonResponseException) { + GoogleJsonError jsonError = ((GoogleJsonResponseException) exception).getDetails(); + if (jsonError != null) { + BaseServiceException.Error error = + new BaseServiceException.Error(jsonError.getCode(), reason(jsonError)); + code = error.getCode(); + reason = error.getReason(); + retryable = error.isRetryable(idempotent, retryableErrors); + if (reason != null) { + GoogleJsonError.ErrorInfo errorInfo = jsonError.getErrors().get(0); + location = errorInfo.getLocation(); + debugInfo = (String) errorInfo.get("debugInfo"); + } + } else { + code = ((GoogleJsonResponseException) exception).getStatusCode(); + retryable = BaseServiceException.isRetryable(code, null, idempotent, retryableErrors); + } + } else { + // In cases where an exception is an instance of HttpResponseException but not + // an instance of GoogleJsonResponseException, check the status code to determine whether + // it's retryable + code = ((HttpResponseException) exception).getStatusCode(); + retryable = BaseServiceException.isRetryable(code, null, idempotent, retryableErrors); + } + } + return ExceptionData.newBuilder() + .setMessage(message(exception)) + .setCause(exception) + .setRetryable( + MoreObjects.firstNonNull( + retryable, BaseServiceException.isRetryable(idempotent, exception))) + .setCode(code) + .setReason(reason) + .setLocation(location) + .setDebugInfo(debugInfo) + .build(); + } + + @InternalApi("This class should only be extended within google-cloud-java") + protected BaseHttpServiceException( + GoogleJsonError googleJsonError, + boolean idempotent, + Set retryableErrors) { + super(makeExceptionData(googleJsonError, idempotent, retryableErrors)); + } + + private static ExceptionData makeExceptionData( + GoogleJsonError googleJsonError, + boolean idempotent, + Set retryableErrors) { + int code = googleJsonError.getCode(); + String reason = reason(googleJsonError); + + ExceptionData.Builder exceptionData = ExceptionData.newBuilder(); + exceptionData + .setMessage(googleJsonError.getMessage()) + .setCause(null) + .setRetryable(BaseServiceException.isRetryable(code, reason, idempotent, retryableErrors)) + .setCode(code) + .setReason(reason); + if (reason != null) { + GoogleJsonError.ErrorInfo errorInfo = googleJsonError.getErrors().get(0); + exceptionData.setLocation(errorInfo.getLocation()); + exceptionData.setDebugInfo((String) errorInfo.get("debugInfo")); + } else { + exceptionData.setLocation(null); + exceptionData.setDebugInfo(null); + } + return exceptionData.build(); + } + + @InternalApi("This class should only be extended within google-cloud-java") + protected BaseHttpServiceException( + int code, + String message, + String reason, + boolean idempotent, + Set retryableErrors) { + this(code, message, reason, idempotent, retryableErrors, null); + } + + @InternalApi("This class should only be extended within google-cloud-java") + protected BaseHttpServiceException( + int code, + String message, + String reason, + boolean idempotent, + Set retryableErrors, + Throwable cause) { + super( + ExceptionData.newBuilder() + .setMessage(message) + .setCause(cause) + .setRetryable( + BaseServiceException.isRetryable(code, reason, idempotent, retryableErrors)) + .setCode(code) + .setReason(reason) + .setLocation(null) + .setDebugInfo(null) + .build()); + } + + private static String reason(GoogleJsonError error) { + if (error.getErrors() != null && !error.getErrors().isEmpty()) { + return error.getErrors().get(0).getReason(); + } + return null; + } + + private static String message(IOException exception) { + if (exception instanceof GoogleJsonResponseException) { + GoogleJsonError details = ((GoogleJsonResponseException) exception).getDetails(); + if (details != null) { + return details.getMessage(); + } + } + return exception.getMessage(); + } +} diff --git a/google-cloud-core-http/src/main/java/com/google/cloud/http/CensusHttpModule.java b/google-cloud-core-http/src/main/java/com/google/cloud/http/CensusHttpModule.java new file mode 100644 index 0000000000..d141507cc0 --- /dev/null +++ b/google-cloud-core-http/src/main/java/com/google/cloud/http/CensusHttpModule.java @@ -0,0 +1,156 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.http; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpExecuteInterceptor; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.common.annotations.VisibleForTesting; +import io.opencensus.contrib.http.util.HttpPropagationUtil; +import io.opencensus.trace.SpanContext; +import io.opencensus.trace.Tracer; +import io.opencensus.trace.propagation.TextFormat; +import java.io.IOException; +import javax.annotation.Nullable; + +/** + * Provides utilities for Open Census to record http stats/trace information and propagate tracing + * context. + */ +public final class CensusHttpModule { + + /** + * OpenCensus tracing component. When no OpenCensus implementation is provided, it will return a + * no-op tracer. + */ + private final Tracer tracer; + + /** {@link TextFormat} used in tracing context propagation. */ + @Nullable private final TextFormat propagationTextFormat; + + /** {@link TextFormat.Setter} for {@link #propagationTextFormat}. */ + @Nullable private final TextFormat.Setter propagationTextFormatSetter; + + /** Whether spans are stored locally. */ + private final boolean isRecordEvents; + + /** Default HTTP propagation text formatter. */ + @VisibleForTesting + static final class DefaultPropagationTextFormatSetter extends TextFormat.Setter { + static final TextFormat.Setter INSTANCE = new DefaultPropagationTextFormatSetter(); + + @Override + public void put(HttpHeaders carrier, String key, String value) { + carrier.set(key, value); + } + } + + /** + * An {@link HttpExecuteInterceptor} implementation to inject HTTP request and add getContext + * information before it is executed. + */ + @VisibleForTesting + final class CensusHttpExecuteInterceptor implements HttpExecuteInterceptor { + @Nullable HttpExecuteInterceptor interceptor; + + CensusHttpExecuteInterceptor(HttpExecuteInterceptor interceptor) { + this.interceptor = interceptor; + } + + @Override + public void intercept(HttpRequest request) throws IOException { + checkNotNull(request); + if (this.interceptor != null) { + this.interceptor.intercept(request); + } + if (propagationTextFormat != null && propagationTextFormatSetter != null) { + SpanContext spanContext = tracer.getCurrentSpan().getContext(); + if (!SpanContext.INVALID.equals(spanContext)) { + propagationTextFormat.inject( + spanContext, request.getHeaders(), propagationTextFormatSetter); + } + } + } + } + + /** + * An {@link HttpRequestInitializer} implementation to set {@link CensusHttpExecuteInterceptor} as + * interceptor. + */ + @VisibleForTesting + final class CensusHttpRequestInitializer implements HttpRequestInitializer { + @Nullable HttpRequestInitializer initializer; + + CensusHttpRequestInitializer(HttpRequestInitializer initializer) { + this.initializer = initializer; + } + + @Override + public void initialize(HttpRequest request) throws IOException { + checkNotNull(request); + if (this.initializer != null) { + this.initializer.initialize(request); + } + request.setInterceptor(new CensusHttpExecuteInterceptor(request.getInterceptor())); + } + } + + /** + * Creates a {@link CensusHttpModule} with given parameters. + * + * @param tracer the OpenCensus {@code Tracer}. + * @param isRecordEvents whether spans are stored locally. + */ + public CensusHttpModule(Tracer tracer, boolean isRecordEvents) { + checkNotNull(tracer, "tracer"); + this.tracer = tracer; + this.isRecordEvents = isRecordEvents; + this.propagationTextFormat = HttpPropagationUtil.getCloudTraceFormat(); + this.propagationTextFormatSetter = DefaultPropagationTextFormatSetter.INSTANCE; + } + + /** + * Returns the tracing component of OpenCensus. + * + * @return the tracing component of OpenCensus. + */ + public Tracer getTracer() { + return tracer; + } + + /** + * Returns whether spans are stored locally. + * + * @return whether spans are stored locally. + */ + public boolean isRecordEvents() { + return isRecordEvents; + } + + /** + * Returns the {@link HttpExecuteInterceptor} used when initializing the {@link HttpRequest}. + * + * @param initializer the original initializer which will be executed before this initializer. + * @return the {@code HttpExecuteInterceptor}. + */ + public HttpRequestInitializer getHttpRequestInitializer(HttpRequestInitializer initializer) { + return new CensusHttpRequestInitializer(initializer); + } +} diff --git a/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java b/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java new file mode 100644 index 0000000000..2dbcb018f8 --- /dev/null +++ b/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java @@ -0,0 +1,229 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.http; + +import static com.google.common.base.MoreObjects.firstNonNull; + +import com.google.api.client.extensions.appengine.http.UrlFetchTransport; +import com.google.api.client.http.HttpRequest; +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.gax.core.GaxProperties; +import com.google.api.gax.httpjson.HttpHeadersUtils; +import com.google.api.gax.rpc.ApiClientHeaderProvider; +import com.google.api.gax.rpc.HeaderProvider; +import com.google.auth.Credentials; +import com.google.auth.http.HttpCredentialsAdapter; +import com.google.auth.http.HttpTransportFactory; +import com.google.cloud.NoCredentials; +import com.google.cloud.PlatformInformation; +import com.google.cloud.ServiceOptions; +import com.google.cloud.TransportOptions; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.Objects; + +/** Class representing service options for those services that use HTTP as the transport layer. */ +public class HttpTransportOptions implements TransportOptions { + + private static final long serialVersionUID = 7890117765045419810L; + private final int connectTimeout; + private final int readTimeout; + private final String httpTransportFactoryClassName; + + private transient HttpTransportFactory httpTransportFactory; + + public static class DefaultHttpTransportFactory implements HttpTransportFactory { + + private static final HttpTransportFactory INSTANCE = new DefaultHttpTransportFactory(); + + @Override + public HttpTransport create() { + // Consider App Engine Standard + if (PlatformInformation.isOnGAEStandard7()) { + try { + return new UrlFetchTransport(); + } catch (Exception ignore) { + // Maybe not on App Engine + } + } + return new NetHttpTransport(); + } + } + + /** Builder for {@code HttpTransportOptions}. */ + public static class Builder { + + private HttpTransportFactory httpTransportFactory; + private int connectTimeout = -1; + private int readTimeout = -1; + + private Builder() {} + + private Builder(HttpTransportOptions options) { + httpTransportFactory = options.httpTransportFactory; + connectTimeout = options.connectTimeout; + readTimeout = options.readTimeout; + } + + public HttpTransportOptions build() { + return new HttpTransportOptions(this); + } + + /** + * Sets the HTTP transport factory. + * + * @return the builder + */ + public Builder setHttpTransportFactory(HttpTransportFactory httpTransportFactory) { + this.httpTransportFactory = httpTransportFactory; + return this; + } + + /** + * Sets the timeout in milliseconds to establish a connection. + * + * @param connectTimeout connection timeout in milliseconds. 0 for an infinite timeout, a + * negative number for the default value (20000). + * @return the builder + */ + public Builder setConnectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + /** + * Sets the timeout in milliseconds to read data from an established connection. + * + * @param readTimeout read timeout in milliseconds. 0 for an infinite timeout, a negative number + * for the default value (20000). + * @return the builder + */ + public Builder setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + return this; + } + } + + public HttpTransportOptions(Builder builder) { + httpTransportFactory = + firstNonNull( + builder.httpTransportFactory, + ServiceOptions.getFromServiceLoader( + HttpTransportFactory.class, DefaultHttpTransportFactory.INSTANCE)); + httpTransportFactoryClassName = httpTransportFactory.getClass().getName(); + connectTimeout = builder.connectTimeout; + readTimeout = builder.readTimeout; + } + + /** Returns the HTTP transport factory. */ + public HttpTransportFactory getHttpTransportFactory() { + return httpTransportFactory; + } + + /** + * Returns a request initializer responsible for initializing requests according to service + * options. + */ + public HttpRequestInitializer getHttpRequestInitializer( + final ServiceOptions serviceOptions) { + Credentials scopedCredentials = serviceOptions.getScopedCredentials(); + final HttpRequestInitializer delegate = + scopedCredentials != null && scopedCredentials != NoCredentials.getInstance() + ? new HttpCredentialsAdapter(scopedCredentials) + : null; + HeaderProvider internalHeaderProvider = + getInternalHeaderProviderBuilder(serviceOptions).build(); + final HeaderProvider headerProvider = + serviceOptions.getMergedHeaderProvider(internalHeaderProvider); + + return new HttpRequestInitializer() { + @Override + public void initialize(HttpRequest httpRequest) throws IOException { + if (delegate != null) { + delegate.initialize(httpRequest); + } + if (connectTimeout >= 0) { + httpRequest.setConnectTimeout(connectTimeout); + } + if (readTimeout >= 0) { + httpRequest.setReadTimeout(readTimeout); + } + + HttpHeadersUtils.setHeaders(httpRequest.getHeaders(), headerProvider.getHeaders()); + } + }; + } + + ApiClientHeaderProvider.Builder getInternalHeaderProviderBuilder( + ServiceOptions serviceOptions) { + ApiClientHeaderProvider.Builder builder = ApiClientHeaderProvider.newBuilder(); + builder.setClientLibToken( + ServiceOptions.getGoogApiClientLibName(), + GaxProperties.getLibraryVersion(serviceOptions.getClass())); + return builder; + } + + /** + * Returns the timeout in milliseconds to establish a connection. 0 is an infinite timeout, a + * negative number is the default value (20000). + */ + public int getConnectTimeout() { + return connectTimeout; + } + + /** + * Returns the timeout in milliseconds to read from an established connection. 0 is an infinite + * timeout, a negative number is the default value (20000). + */ + public int getReadTimeout() { + return readTimeout; + } + + public Builder toBuilder() { + return new Builder(this); + } + + @Override + public int hashCode() { + return Objects.hash(httpTransportFactoryClassName, connectTimeout, readTimeout); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + HttpTransportOptions other = (HttpTransportOptions) obj; + return Objects.equals(httpTransportFactoryClassName, other.httpTransportFactoryClassName) + && Objects.equals(connectTimeout, other.connectTimeout) + && Objects.equals(readTimeout, other.readTimeout); + } + + private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { + input.defaultReadObject(); + httpTransportFactory = ServiceOptions.newInstance(httpTransportFactoryClassName); + } + + public static Builder newBuilder() { + return new Builder(); + } +} diff --git a/google-cloud-core-http/src/test/java/com/google/cloud/http/BaseHttpServiceExceptionTest.java b/google-cloud-core-http/src/test/java/com/google/cloud/http/BaseHttpServiceExceptionTest.java new file mode 100644 index 0000000000..e88a32a9af --- /dev/null +++ b/google-cloud-core-http/src/test/java/com/google/cloud/http/BaseHttpServiceExceptionTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.http; + +import static com.google.cloud.BaseServiceException.UNKNOWN_CODE; +import static org.easymock.EasyMock.createMock; +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.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.cloud.BaseServiceException; +import com.google.cloud.RetryHelper; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.util.Collections; +import java.util.Set; +import org.junit.Test; + +public class BaseHttpServiceExceptionTest { + + private static final int CODE = 1; + private static final int CODE_NO_REASON = 2; + private static final String MESSAGE = "some message"; + private static final String REASON = "some reason"; + private static final boolean RETRYABLE = true; + private static final boolean IDEMPOTENT = true; + private static final boolean NOT_IDEMPOTENT = false; + private static final Set EMPTY_RETRYABLE_ERRORS = + Collections.emptySet(); + + private static class CustomServiceException extends BaseHttpServiceException { + + private static final long serialVersionUID = -195251309124875103L; + + public CustomServiceException(int code, String message, String reason, boolean idempotent) { + super(code, message, reason, idempotent, RETRYABLE_ERRORS); + } + + private static final Set RETRYABLE_ERRORS = + ImmutableSet.of( + new Error(CODE, REASON), new Error(null, REASON), new Error(CODE_NO_REASON, null)); + } + + @Test + public void testBaseServiceException() { + BaseServiceException serviceException = + new BaseHttpServiceException(CODE, MESSAGE, REASON, IDEMPOTENT, EMPTY_RETRYABLE_ERRORS); + assertEquals(CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(REASON, serviceException.getReason()); + assertFalse(serviceException.isRetryable()); + assertNull(serviceException.getCause()); + + serviceException = + new BaseHttpServiceException(CODE, MESSAGE, REASON, IDEMPOTENT, EMPTY_RETRYABLE_ERRORS); + assertEquals(CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(REASON, serviceException.getReason()); + assertFalse(serviceException.isRetryable()); + assertNull(serviceException.getCause()); + + Exception cause = new RuntimeException(); + serviceException = + new BaseHttpServiceException( + CODE, MESSAGE, REASON, IDEMPOTENT, EMPTY_RETRYABLE_ERRORS, cause); + assertEquals(CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(REASON, serviceException.getReason()); + assertFalse(serviceException.isRetryable()); + assertEquals(cause, serviceException.getCause()); + + serviceException = + new BaseHttpServiceException( + CODE, MESSAGE, REASON, NOT_IDEMPOTENT, EMPTY_RETRYABLE_ERRORS, cause); + assertEquals(CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(REASON, serviceException.getReason()); + assertFalse(serviceException.isRetryable()); + assertEquals(cause, serviceException.getCause()); + + IOException exception = new SocketTimeoutException(); + serviceException = new BaseHttpServiceException(exception, IDEMPOTENT, EMPTY_RETRYABLE_ERRORS); + assertTrue(serviceException.isRetryable()); + assertNull(serviceException.getMessage()); + assertEquals(exception, serviceException.getCause()); + + exception = new SocketException(); + serviceException = new BaseHttpServiceException(exception, IDEMPOTENT, EMPTY_RETRYABLE_ERRORS); + assertTrue(serviceException.isRetryable()); + assertNull(serviceException.getMessage()); + assertEquals(exception, serviceException.getCause()); + + exception = new IOException("insufficient data written"); + serviceException = new BaseHttpServiceException(exception, IDEMPOTENT, EMPTY_RETRYABLE_ERRORS); + assertTrue(serviceException.isRetryable()); + assertEquals("insufficient data written", serviceException.getMessage()); + assertEquals(exception, serviceException.getCause()); + + GoogleJsonError error = new GoogleJsonError(); + error.setCode(CODE); + error.setMessage(MESSAGE); + serviceException = new BaseHttpServiceException(error, IDEMPOTENT, EMPTY_RETRYABLE_ERRORS); + assertEquals(CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertFalse(serviceException.isRetryable()); + + serviceException = new CustomServiceException(CODE, MESSAGE, REASON, IDEMPOTENT); + assertEquals(CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(REASON, serviceException.getReason()); + assertEquals(RETRYABLE, serviceException.isRetryable()); + + serviceException = new CustomServiceException(CODE_NO_REASON, MESSAGE, null, IDEMPOTENT); + assertEquals(CODE_NO_REASON, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertNull(serviceException.getReason()); + assertEquals(RETRYABLE, serviceException.isRetryable()); + + serviceException = new CustomServiceException(UNKNOWN_CODE, MESSAGE, REASON, IDEMPOTENT); + assertEquals(UNKNOWN_CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(REASON, serviceException.getReason()); + assertEquals(RETRYABLE, serviceException.isRetryable()); + } + + @Test + public void testTranslateAndThrow() throws Exception { + BaseServiceException cause = + new BaseHttpServiceException(CODE, MESSAGE, REASON, IDEMPOTENT, EMPTY_RETRYABLE_ERRORS); + RetryHelper.RetryHelperException exceptionMock = + createMock(RetryHelper.RetryHelperException.class); + expect(exceptionMock.getCause()).andReturn(cause).times(2); + replay(exceptionMock); + try { + BaseServiceException.translate(exceptionMock); + } catch (BaseServiceException ex) { + assertEquals(CODE, ex.getCode()); + assertEquals(MESSAGE, ex.getMessage()); + assertFalse(ex.isRetryable()); + } finally { + verify(exceptionMock); + } + } +} diff --git a/google-cloud-core-http/src/test/java/com/google/cloud/http/CensusHttpModuleTest.java b/google-cloud-core-http/src/test/java/com/google/cloud/http/CensusHttpModuleTest.java new file mode 100644 index 0000000000..c04d0647fb --- /dev/null +++ b/google-cloud-core-http/src/test/java/com/google/cloud/http/CensusHttpModuleTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.http; + +import static com.google.common.truth.Truth.assertThat; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.createMockBuilder; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpExecuteInterceptor; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.javanet.NetHttpTransport; +import io.opencensus.common.Scope; +import io.opencensus.trace.Span; +import io.opencensus.trace.SpanContext; +import io.opencensus.trace.SpanId; +import io.opencensus.trace.TraceId; +import io.opencensus.trace.TraceOptions; +import io.opencensus.trace.Tracer; +import io.opencensus.trace.Tracestate; +import io.opencensus.trace.Tracing; +import io.opencensus.trace.propagation.TextFormat; +import java.io.IOException; +import java.util.EnumSet; +import java.util.Random; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link CensusHttpModule}. */ +@RunWith(JUnit4.class) +public class CensusHttpModuleTest { + + @Rule public final ExpectedException thrown = ExpectedException.none(); + + private final Tracer tracer = Tracing.getTracer(); + private final CensusHttpModule censusHttpModule = new CensusHttpModule(tracer, false); + private HttpRequest httpRequest; + + @Before + public void setUp() throws IOException { + httpRequest = + new NetHttpTransport() + .createRequestFactory() + .buildRequest("GET", new GenericUrl("https://www.google.com"), null); + } + + @Test + public void tracerShouldNotBeNull() { + assertThat(censusHttpModule.getTracer()).isNotNull(); + } + + @Test + public void isRecordEventsShouldBeSet() { + assertThat(censusHttpModule.isRecordEvents()).isEqualTo(false); + } + + @Test + public void getHttpRequestInitializerShouldReturnCorrectClass() { + HttpRequestInitializer initializer = censusHttpModule.getHttpRequestInitializer(null); + assertThat(initializer).isInstanceOf(CensusHttpModule.CensusHttpRequestInitializer.class); + } + + @Test + public void implementationOfDefaultTextFormatSetter() { + String testKey = "testKey"; + String testValue = "testValue"; + TextFormat.Setter setter = + CensusHttpModule.DefaultPropagationTextFormatSetter.INSTANCE; + setter.put(httpRequest.getHeaders(), testKey, testValue); + assertThat(httpRequest.getHeaders().get(testKey)).isEqualTo(testValue); + } + + @Test + public void censusHttpExecuteInterceptorDisallowNullRequest() throws IOException { + HttpExecuteInterceptor interceptor = censusHttpModule.new CensusHttpExecuteInterceptor(null); + thrown.expect(NullPointerException.class); + interceptor.intercept(null); + } + + @Test + public void censusHttpExecuteInterceptorShouldExecuteOriginal() throws IOException { + HttpExecuteInterceptor mockInterceptor = createMock(HttpExecuteInterceptor.class); + HttpExecuteInterceptor censusInterceptor = + censusHttpModule.new CensusHttpExecuteInterceptor(mockInterceptor); + mockInterceptor.intercept(httpRequest); + replay(mockInterceptor); + censusInterceptor.intercept(httpRequest); + verify(mockInterceptor); + } + + @Test + public void censusHttpExecuteInterceptorShouldInjectHeader() throws IOException { + Random random = new Random(); + SpanContext spanContext = + SpanContext.create( + TraceId.generateRandomId(random), + SpanId.generateRandomId(random), + TraceOptions.DEFAULT, + Tracestate.builder().build()); + Span mockSpan = + createMockBuilder(Span.class) + .withConstructor(SpanContext.class, EnumSet.class) + .withArgs(spanContext, null) + .createMock(); + Scope scope = tracer.withSpan(mockSpan); + try { + HttpExecuteInterceptor interceptor = censusHttpModule.new CensusHttpExecuteInterceptor(null); + interceptor.intercept(httpRequest); + assertThat(httpRequest.getHeaders().get("X-Cloud-Trace-Context")).isNotNull(); + } finally { + scope.close(); + } + } + + @Test + public void censusHttpRequestInitializerDisallowNullRequest() throws IOException { + HttpRequestInitializer initializer = censusHttpModule.getHttpRequestInitializer(null); + thrown.expect(NullPointerException.class); + initializer.initialize(null); + } + + @Test + public void censusHttpRequestInitializerShouldExecuteOriginal() throws IOException { + HttpRequestInitializer mockOriginalInitializer = createMock(HttpRequestInitializer.class); + HttpRequestInitializer censusInitializer = + censusHttpModule.getHttpRequestInitializer(mockOriginalInitializer); + mockOriginalInitializer.initialize(httpRequest); + replay(mockOriginalInitializer); + censusInitializer.initialize(httpRequest); + verify(mockOriginalInitializer); + } + + @Test + public void censusHttpRequestInitializerShouldSetInterceptor() throws IOException { + censusHttpModule.getHttpRequestInitializer(null).initialize(httpRequest); + assertThat(httpRequest.getInterceptor()) + .isInstanceOf(CensusHttpModule.CensusHttpExecuteInterceptor.class); + } +} diff --git a/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java b/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java new file mode 100644 index 0000000000..1ff7871aec --- /dev/null +++ b/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.google.api.gax.rpc.HeaderProvider; +import com.google.auth.http.HttpTransportFactory; +import com.google.cloud.ServiceOptions; +import com.google.cloud.http.HttpTransportOptions.DefaultHttpTransportFactory; +import java.util.regex.Pattern; +import org.easymock.EasyMock; +import org.junit.Test; + +public class HttpTransportOptionsTest { + + private static final HttpTransportFactory MOCK_HTTP_TRANSPORT_FACTORY = + EasyMock.createMock(HttpTransportFactory.class); + private static final HttpTransportOptions OPTIONS = + HttpTransportOptions.newBuilder() + .setConnectTimeout(1234) + .setHttpTransportFactory(MOCK_HTTP_TRANSPORT_FACTORY) + .setReadTimeout(5678) + .build(); + private static final HttpTransportOptions DEFAULT_OPTIONS = + HttpTransportOptions.newBuilder().build(); + private static final HttpTransportOptions OPTIONS_COPY = OPTIONS.toBuilder().build(); + + @Test + public void testBuilder() { + assertEquals(1234, OPTIONS.getConnectTimeout()); + assertSame(MOCK_HTTP_TRANSPORT_FACTORY, OPTIONS.getHttpTransportFactory()); + assertEquals(5678, OPTIONS.getReadTimeout()); + assertEquals(-1, DEFAULT_OPTIONS.getConnectTimeout()); + assertTrue(DEFAULT_OPTIONS.getHttpTransportFactory() instanceof DefaultHttpTransportFactory); + assertEquals(-1, DEFAULT_OPTIONS.getReadTimeout()); + } + + @Test + public void testBaseEquals() { + assertEquals(OPTIONS, OPTIONS_COPY); + assertNotEquals(DEFAULT_OPTIONS, OPTIONS); + } + + @Test + public void testBaseHashCode() { + assertEquals(OPTIONS.hashCode(), OPTIONS_COPY.hashCode()); + assertNotEquals(DEFAULT_OPTIONS.hashCode(), OPTIONS.hashCode()); + } + + @Test + public void testHeader() { + String expectedHeaderPattern = "^gl-java/.+ gccl/.* gax/.+"; + ServiceOptions serviceOptions = EasyMock.createMock(ServiceOptions.class); + HeaderProvider headerProvider = + OPTIONS.getInternalHeaderProviderBuilder(serviceOptions).build(); + + assertEquals(1, headerProvider.getHeaders().size()); + assertTrue( + Pattern.compile(expectedHeaderPattern) + .matcher(headerProvider.getHeaders().values().iterator().next()) + .find()); + } +} diff --git a/google-cloud-core/pom.xml b/google-cloud-core/pom.xml new file mode 100644 index 0000000000..97737acf41 --- /dev/null +++ b/google-cloud-core/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + com.google.cloud + google-cloud-core + 1.91.0 + jar + Google Cloud Core + https://github.com/googleapis/google-cloud-java/tree/master/google-cloud-clients/google-cloud-core + + Core module for the google-cloud. + + + com.google.cloud + google-cloud-core-parent + 1.91.0 + + + google-cloud-core + + + + junit + junit + test + + + org.easymock + easymock + test + + + org.objenesis + objenesis + test + + + com.google.api + gax + + + com.google.protobuf + protobuf-java-util + + + com.google.api.grpc + proto-google-common-protos + + + com.google.api.grpc + proto-google-iam-v1 + + + org.threeten + threetenbp + + + com.google.api + api-common + + + com.google.auth + google-auth-library-credentials + + + com.google.auth + google-auth-library-oauth2-http + + + com.google.http-client + google-http-client + + + com.google.http-client + google-http-client-jackson2 + + + com.google.protobuf + protobuf-java + + + com.google.guava + guava + + + + com.google.truth + truth + test + + + com.google.guava + guava-testlib + test + + + diff --git a/google-cloud-core/src/main/java/com/google/cloud/AsyncPageImpl.java b/google-cloud-core/src/main/java/com/google/cloud/AsyncPageImpl.java new file mode 100644 index 0000000000..25cae6f7c8 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/AsyncPageImpl.java @@ -0,0 +1,87 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.InternalApi; +import com.google.api.gax.paging.AsyncPage; +import com.google.api.gax.paging.Page; +import com.google.common.base.Throwables; +import com.google.common.util.concurrent.Uninterruptibles; +import java.io.Serializable; +import java.util.concurrent.ExecutionException; + +/** + * Base implementation for asynchronously consuming Google Cloud paginated results. + * + * @param the value type that the page holds + */ +@InternalApi +public class AsyncPageImpl extends PageImpl implements AsyncPage { + + private static final long serialVersionUID = -6009473188630364906L; + + private final NextPageFetcher asyncPageFetcher; + + /** + * Interface for asynchronously fetching the next page of results from the service. + * + * @param the value type that the page holds + */ + public interface NextPageFetcher extends Serializable { + + ApiFuture> getNextPage(); + } + + private static class SyncNextPageFetcher implements PageImpl.NextPageFetcher { + + private static final long serialVersionUID = -4124568632363525351L; + + private final NextPageFetcher asyncPageFetcher; + + private SyncNextPageFetcher(NextPageFetcher asyncPageFetcher) { + this.asyncPageFetcher = asyncPageFetcher; + } + + @Override + public Page getNextPage() { + try { + return asyncPageFetcher != null + ? Uninterruptibles.getUninterruptibly(asyncPageFetcher.getNextPage()) + : null; + } catch (ExecutionException ex) { + Throwables.throwIfUnchecked(ex.getCause()); + throw new RuntimeException(ex); + } + } + } + + /** Creates an {@code AsyncPageImpl} object. */ + public AsyncPageImpl(NextPageFetcher asyncPageFetcher, String cursor, Iterable results) { + super(new SyncNextPageFetcher(asyncPageFetcher), cursor, results); + this.asyncPageFetcher = asyncPageFetcher; + } + + @Override + public ApiFuture> getNextPageAsync() { + if (getNextPageToken() == null || asyncPageFetcher == null) { + return ApiFutures.immediateFuture(null); + } + return asyncPageFetcher.getNextPage(); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/BaseService.java b/google-cloud-core/src/main/java/com/google/cloud/BaseService.java new file mode 100644 index 0000000000..781f29b421 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/BaseService.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.api.core.InternalApi; +import com.google.cloud.ExceptionHandler.Interceptor; + +/** + * Base class for service objects. + * + * @param the {@code ServiceOptions} subclass corresponding to the service + */ +public abstract class BaseService> + implements Service { + + public static final Interceptor EXCEPTION_HANDLER_INTERCEPTOR = + new Interceptor() { + + private static final long serialVersionUID = -8429573486870467828L; + + @Override + public RetryResult afterEval(Exception exception, RetryResult retryResult) { + return Interceptor.RetryResult.CONTINUE_EVALUATION; + } + + @Override + public RetryResult beforeEval(Exception exception) { + if (exception instanceof BaseServiceException) { + boolean retriable = ((BaseServiceException) exception).isRetryable(); + return retriable + ? Interceptor.RetryResult.RETRY + : Interceptor.RetryResult.CONTINUE_EVALUATION; + } + return Interceptor.RetryResult.CONTINUE_EVALUATION; + } + }; + public static final ExceptionHandler EXCEPTION_HANDLER = + ExceptionHandler.newBuilder() + .abortOn(RuntimeException.class) + .addInterceptors(EXCEPTION_HANDLER_INTERCEPTOR) + .build(); + + private final OptionsT options; + + @InternalApi("This class should only be extended within google-cloud-java") + protected BaseService(OptionsT options) { + this.options = options; + } + + @Override + public OptionsT getOptions() { + return options; + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/BaseServiceException.java b/google-cloud-core/src/main/java/com/google/cloud/BaseServiceException.java new file mode 100644 index 0000000000..e9e9e2aa63 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/BaseServiceException.java @@ -0,0 +1,340 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.api.core.InternalApi; +import com.google.common.base.MoreObjects; +import java.io.IOException; +import java.io.Serializable; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.security.cert.CertificateException; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import javax.net.ssl.SSLHandshakeException; + +/** Base class for all service exceptions. */ +public class BaseServiceException extends RuntimeException { + + private static final long serialVersionUID = 759921776378760835L; + public static final int UNKNOWN_CODE = 0; + + private final int code; + private final boolean retryable; + private final String reason; + private final String location; + private final String debugInfo; + + @InternalApi + public static final class ExceptionData implements Serializable { + private static final long serialVersionUID = 2222230861338426753L; + + private final String message; + private final Throwable cause; + private final int code; + private final boolean retryable; + private final String reason; + private final String location; + private final String debugInfo; + + private ExceptionData( + String message, + Throwable cause, + int code, + boolean retryable, + String reason, + String location, + String debugInfo) { + this.message = message; + this.cause = cause; + this.code = code; + this.retryable = retryable; + this.reason = reason; + this.location = location; + this.debugInfo = debugInfo; + } + + public String getMessage() { + return message; + } + + public Throwable getCause() { + return cause; + } + + public int getCode() { + return code; + } + + public boolean isRetryable() { + return retryable; + } + + public String getReason() { + return reason; + } + + public String getLocation() { + return location; + } + + public String getDebugInfo() { + return debugInfo; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static ExceptionData from(int code, String message, String reason, boolean retryable) { + return from(code, message, reason, retryable, null); + } + + public static ExceptionData from( + int code, String message, String reason, boolean retryable, Throwable cause) { + return newBuilder() + .setCode(code) + .setMessage(message) + .setReason(reason) + .setRetryable(retryable) + .setCause(cause) + .build(); + } + + @InternalApi + public static final class Builder { + + private String message; + private Throwable cause; + private int code; + private boolean retryable; + private String reason; + private String location; + private String debugInfo; + + private Builder() {} + + public Builder setMessage(String message) { + this.message = message; + return this; + } + + public Builder setCause(Throwable cause) { + this.cause = cause; + return this; + } + + public Builder setCode(int code) { + this.code = code; + return this; + } + + public Builder setRetryable(boolean retryable) { + this.retryable = retryable; + return this; + } + + public Builder setReason(String reason) { + this.reason = reason; + return this; + } + + public Builder setLocation(String location) { + this.location = location; + return this; + } + + public Builder setDebugInfo(String debugInfo) { + this.debugInfo = debugInfo; + return this; + } + + public ExceptionData build() { + return new ExceptionData(message, cause, code, retryable, reason, location, debugInfo); + } + } + } + + @InternalApi + public static final class Error implements Serializable { + + private static final long serialVersionUID = -4019600198652965721L; + + private final Integer code; + private final String reason; + private final boolean rejected; + + public Error(Integer code, String reason) { + this(code, reason, false); + } + + public Error(Integer code, String reason, boolean rejected) { + this.code = code; + this.reason = reason; + this.rejected = rejected; + } + + /** Returns the code associated with this exception. */ + public Integer getCode() { + return code; + } + + /** + * Returns true if the error indicates that the API call was certainly not accepted by the + * server. For instance, if the server returns a rate limit exceeded error, it certainly did not + * process the request and this method will return {@code true}. + */ + public boolean isRejected() { + return rejected; + } + + /** Returns the reason that caused the exception. */ + public String getReason() { + return reason; + } + + @InternalApi + public boolean isRetryable(boolean idempotent, Set retryableErrors) { + return BaseServiceException.isRetryable(code, reason, idempotent, retryableErrors); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("code", code) + .add("reason", reason) + .add("rejected", rejected) + .toString(); + } + + @Override + public int hashCode() { + return Objects.hash(code, reason, rejected); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Error)) { + return false; + } + Error o = (Error) obj; + return Objects.equals(code, o.code) + && Objects.equals(reason, o.reason) + && Objects.equals(rejected, o.rejected); + } + } + + @InternalApi + public static boolean isRetryable( + Integer code, String reason, boolean idempotent, Set retryableErrors) { + for (Error retryableError : retryableErrors) { + if ((retryableError.getCode() == null || retryableError.getCode().equals(code)) + && (retryableError.getReason() == null || retryableError.getReason().equals(reason))) { + return idempotent || retryableError.isRejected(); + } + } + return false; + } + + @InternalApi + public static boolean isRetryable(boolean idempotent, IOException exception) { + boolean exceptionIsRetryable = + exception instanceof SocketTimeoutException + || exception instanceof SocketException + || (exception instanceof SSLHandshakeException + && !(exception.getCause() instanceof CertificateException)) + || "insufficient data written".equals(exception.getMessage()) + || "Error writing request body to server".equals(exception.getMessage()); + return idempotent && exceptionIsRetryable; + } + + @InternalApi + public static void translate(RetryHelper.RetryHelperException ex) { + if (ex.getCause() instanceof BaseServiceException) { + throw (BaseServiceException) ex.getCause(); + } + } + + @InternalApi + public static void translate(ExecutionException ex) { + if (ex.getCause() instanceof BaseServiceException) { + throw (BaseServiceException) ex.getCause(); + } + } + + @InternalApi("This class should only be extended within google-cloud-java") + protected BaseServiceException(ExceptionData exceptionData) { + super(exceptionData.getMessage(), exceptionData.getCause()); + this.code = exceptionData.getCode(); + this.reason = exceptionData.getReason(); + this.retryable = exceptionData.isRetryable(); + this.location = exceptionData.getLocation(); + this.debugInfo = exceptionData.getDebugInfo(); + } + + /** Returns the code associated with this exception. */ + public int getCode() { + return code; + } + + /** Returns the reason that caused the exception. */ + public String getReason() { + return reason; + } + + /** Returns {@code true} when it is safe to retry the operation that caused this exception. */ + public boolean isRetryable() { + return retryable; + } + + /** + * Returns the service location where the error causing the exception occurred. Returns {@code + * null} if not available. + */ + public String getLocation() { + return location; + } + + @InternalApi + public String getDebugInfo() { + return debugInfo; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof BaseServiceException)) { + return false; + } + BaseServiceException other = (BaseServiceException) obj; + return Objects.equals(getCause(), other.getCause()) + && Objects.equals(getMessage(), other.getMessage()) + && code == other.code + && retryable == other.retryable + && Objects.equals(reason, other.reason) + && Objects.equals(location, other.location) + && Objects.equals(debugInfo, other.debugInfo); + } + + @Override + public int hashCode() { + return Objects.hash(getCause(), getMessage(), code, retryable, reason, location, debugInfo); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/BaseWriteChannel.java b/google-cloud-core/src/main/java/com/google/cloud/BaseWriteChannel.java new file mode 100644 index 0000000000..865af88959 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/BaseWriteChannel.java @@ -0,0 +1,332 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.api.core.InternalApi; +import java.io.IOException; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Base implementation for a {@link WriteChannel}. + * + * @param the service options used by the channel to issue RPC requests + * @param the entity this channel writes data to. Possibly with additional configuration + */ +public abstract class BaseWriteChannel< + ServiceOptionsT extends ServiceOptions, EntityT extends Serializable> + implements WriteChannel { + + private static final int MIN_CHUNK_SIZE = 256 * 1024; + private static final int DEFAULT_CHUNK_SIZE = 8 * MIN_CHUNK_SIZE; + + private final ServiceOptionsT options; + private final EntityT entity; + private final String uploadId; + private long position; + private byte[] buffer = new byte[0]; + private int limit; + private boolean isOpen = true; + private int chunkSize = getDefaultChunkSize(); + + protected int getMinChunkSize() { + return MIN_CHUNK_SIZE; + } + + protected int getDefaultChunkSize() { + return DEFAULT_CHUNK_SIZE; + } + + /** + * Writes {@code length} bytes of {@link #getBuffer()} to the {@link #getUploadId()} URL. + * + * @param length the number of bytes to write from {@link #getBuffer()} + * @param last if {@code true} the resumable session is closed + */ + protected abstract void flushBuffer(int length, boolean last); + + protected ServiceOptionsT getOptions() { + return options; + } + + protected EntityT getEntity() { + return entity; + } + + protected String getUploadId() { + return uploadId; + } + + protected long getPosition() { + return position; + } + + protected byte[] getBuffer() { + return buffer; + } + + protected int getLimit() { + return limit; + } + + protected int getChunkSize() { + return chunkSize; + } + + @Override + public final void setChunkSize(int chunkSize) { + int minSize = getMinChunkSize(); + + this.chunkSize = Math.max(minSize, (chunkSize + minSize - 1) / minSize * minSize); + } + + @InternalApi("This class should only be extended within google-cloud-java") + protected BaseWriteChannel(ServiceOptionsT options, EntityT entity, String uploadId) { + this.options = options; + this.entity = entity; + this.uploadId = uploadId; + } + + private void flush() { + if (limit >= chunkSize) { + final int length = limit - limit % getMinChunkSize(); + flushBuffer(length, false); + position += length; + limit -= length; + byte[] temp = new byte[chunkSize]; + System.arraycopy(buffer, length, temp, 0, limit); + buffer = temp; + } + } + + private void validateOpen() throws ClosedChannelException { + if (!isOpen) { + throw new ClosedChannelException(); + } + } + + @Override + public final 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(); + return toWrite; + } + + @Override + public boolean isOpen() { + return isOpen; + } + + @Override + public final void close() throws IOException { + if (isOpen) { + flushBuffer(limit, true); + position += buffer.length; + isOpen = false; + buffer = null; + } + } + + /** Creates a {@link BaseState.Builder} for the current write channel. */ + protected abstract BaseState.Builder stateBuilder(); + + @Override + public RestorableState capture() { + byte[] bufferToSave = null; + if (isOpen) { + bufferToSave = Arrays.copyOf(buffer, limit); + } + return stateBuilder() + .setPosition(position) + .setBuffer(bufferToSave) + .setIsOpen(isOpen) + .setChunkSize(chunkSize) + .build(); + } + + /** Restores the state of the current write channel given a {@link BaseState} object. */ + protected void restore(BaseState state) { + if (state.buffer != null) { + this.buffer = state.buffer.clone(); + this.limit = state.buffer.length; + } + this.position = state.position; + this.isOpen = state.isOpen; + this.chunkSize = state.chunkSize; + } + + protected abstract static class BaseState< + ServiceOptionsT extends ServiceOptions, EntityT extends Serializable> + implements RestorableState, Serializable { + + private static final long serialVersionUID = 8541062465055125619L; + + protected final ServiceOptionsT serviceOptions; + protected final EntityT entity; + protected final String uploadId; + protected final long position; + protected final byte[] buffer; + protected final boolean isOpen; + protected final int chunkSize; + + @InternalApi("This class should only be extended within google-cloud-java") + protected BaseState(Builder builder) { + this.serviceOptions = builder.serviceOptions; + this.entity = builder.entity; + this.uploadId = builder.uploadId; + this.position = builder.position; + this.buffer = builder.buffer; + this.isOpen = builder.isOpen; + this.chunkSize = builder.chunkSize; + } + + /** + * Base builder for a write channel's state. Users are not supposed to access this class + * directly. + * + * @param the service options used by the channel to issue RPC requests + * @param the entity this channel writes data to. Possibly with additional + * configuration + */ + public abstract static class Builder< + ServiceOptionsT extends ServiceOptions, EntityT extends Serializable> { + private final ServiceOptionsT serviceOptions; + private final EntityT entity; + private final String uploadId; + private long position; + private byte[] buffer; + private boolean isOpen; + private int chunkSize; + + @InternalApi("This class should only be extended within google-cloud-java") + protected Builder(ServiceOptionsT options, EntityT entity, String uploadId) { + this.serviceOptions = options; + this.entity = entity; + this.uploadId = uploadId; + } + + public Builder setPosition(long position) { + this.position = position; + return this; + } + + public Builder setBuffer(byte[] buffer) { + this.buffer = buffer; + return this; + } + + public Builder setIsOpen(boolean isOpen) { + this.isOpen = isOpen; + return this; + } + + public Builder setChunkSize(int chunkSize) { + this.chunkSize = chunkSize; + return this; + } + + public abstract RestorableState build(); + } + + @Override + public int hashCode() { + return Objects.hash( + serviceOptions, entity, uploadId, position, isOpen, chunkSize, Arrays.hashCode(buffer)); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof BaseState)) { + return false; + } + final BaseState other = (BaseState) obj; + return Objects.equals(this.serviceOptions, other.serviceOptions) + && Objects.equals(this.entity, other.entity) + && Objects.equals(this.uploadId, other.uploadId) + && Objects.deepEquals(this.buffer, other.buffer) + && this.position == other.position + && this.isOpen == other.isOpen + && this.chunkSize == other.chunkSize; + } + + protected static final class ValueHolder { + final String name; + final Object value; + + private ValueHolder(String name, Object value) { + this.name = name; + this.value = value; + } + + public static ValueHolder create(String name, Object value) { + return new ValueHolder(name, value); + } + + @Override + public String toString() { + String result = name + "="; + if (value != null && value.getClass().isArray()) { + Object[] objectArray = new Object[] {value}; + String arrayString = Arrays.deepToString(objectArray); + result += arrayString.substring(1, arrayString.length() - 1); + } else { + result += value; + } + return result; + } + } + + protected List toStringHelper() { + List valueList = new ArrayList<>(); + valueList.add(ValueHolder.create("entity", entity)); + valueList.add(ValueHolder.create("uploadId", uploadId)); + valueList.add(ValueHolder.create("position", String.valueOf(position))); + valueList.add(ValueHolder.create("isOpen", String.valueOf(isOpen))); + return valueList; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(getClass().getSimpleName()).append('{'); + String nextSeparator = ""; + for (ValueHolder valueHolder : toStringHelper()) { + builder.append(nextSeparator).append(valueHolder); + nextSeparator = ", "; + } + builder.append('}'); + return builder.toString(); + } + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/BatchResult.java b/google-cloud-core/src/main/java/com/google/cloud/BatchResult.java new file mode 100644 index 0000000000..7281571f91 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/BatchResult.java @@ -0,0 +1,103 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.api.core.InternalApi; +import java.util.LinkedList; +import java.util.List; + +/** + * This class holds a single result of a batch call. The class is not thread-safe. + * + * @param the type of the result + * @param the type of the service-dependent exception thrown when a processing error occurs + */ +public abstract class BatchResult { + + private T result; + private boolean completed = false; + private E error; + private final List> toBeNotified = new LinkedList<>(); + + @InternalApi("This class should only be extended within google-cloud-java") + protected BatchResult() {} + + /** + * Returns {@code true} if the batch has been completed and the result is available; {@code false} + * otherwise. + */ + public boolean completed() { + return completed; + } + + /** + * Returns the result of this call. + * + * @throws IllegalStateException if the batch has not been completed yet + * @throws E if an error occurred when processing the batch request + */ + public T get() throws E { + checkState(completed(), "Batch has not been completed yet"); + if (error != null) { + throw error; + } + return result; + } + + /** + * Adds a callback for the batch operation. + * + * @throws IllegalStateException if the batch has been completed already + */ + public void notify(Callback callback) { + checkState( + !completed, + "The batch has been completed. All the calls to the notify()" + + " method should be done prior to submitting the batch."); + toBeNotified.add(callback); + } + + /** Sets an error and status as completed. Notifies all callbacks. */ + protected void error(E error) { + this.error = checkNotNull(error); + this.completed = true; + for (Callback callback : toBeNotified) { + callback.error(error); + } + } + + /** Sets a result and status as completed. Notifies all callbacks. */ + protected void success(T result) { + this.result = result; + this.completed = true; + for (Callback callback : toBeNotified) { + callback.success(result); + } + } + + /** An interface for the batch callbacks. */ + public interface Callback { + /** The method to be called when the batched operation succeeds. */ + void success(T result); + + /** The method to be called when the batched operation fails. */ + void error(E exception); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/ByteArray.java b/google-cloud-core/src/main/java/com/google/cloud/ByteArray.java new file mode 100644 index 0000000000..5841c541c4 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/ByteArray.java @@ -0,0 +1,153 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.api.core.BetaApi; +import com.google.common.base.MoreObjects; +import com.google.common.base.MoreObjects.ToStringHelper; +import com.google.common.io.BaseEncoding; +import com.google.protobuf.ByteString; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; + +/** An immutable byte array holder. */ +@BetaApi +public class ByteArray implements Iterable, Serializable { + + private static final long serialVersionUID = -1908809133893782840L; + private static final BaseEncoding encoder = BaseEncoding.base64(); + + private final ByteString byteString; + + ByteArray(ByteString byteString) { + this.byteString = byteString; + } + + @Override + public final Iterator iterator() { + return byteString.iterator(); + } + + @Override + public String toString() { + ToStringHelper toStringHelper = MoreObjects.toStringHelper(this); + StringBuilder stBuilder = new StringBuilder(); + for (int i = 0; i < Math.min(256, byteString.size()); i++) { + stBuilder.append(String.format("%02x", byteString.byteAt(i))); + } + if (byteString.size() > 256) { + stBuilder.append("..."); + } + return toStringHelper.add("bytes", stBuilder.toString()).toString(); + } + + @Override + public final int hashCode() { + return byteString.hashCode(); + } + + @Override + public final boolean equals(Object obj) { + return obj == this + || obj instanceof ByteArray && byteString.equals(((ByteArray) obj).byteString); + } + + /** Returns the number of bytes in this {@code ByteArray}. */ + public final int length() { + return byteString.size(); + } + + /** Returns a copy of this {@code ByteArray} as an array of bytes. */ + public final byte[] toByteArray() { + return byteString.toByteArray(); + } + + /** Returns a copy of this {@code ByteArray} as an {@code UTF-8} string. */ + public final String toStringUtf8() { + return byteString.toStringUtf8(); + } + + /** Converts this byte array to its base64 representation. */ + public final String toBase64() { + return encoder.encode(toByteArray()); + } + + /** Returns the content of this {@code ByteArray} as a read-only {@link ByteBuffer}. */ + public final ByteBuffer asReadOnlyByteBuffer() { + return byteString.asReadOnlyByteBuffer(); + } + + /** Returns an {@link InputStream} for this {@code ByteArray} content. */ + public final InputStream asInputStream() { + return byteString.newInput(); + } + + /** + * Copies the content of this {@code ByteArray} into an existing {@code ByteBuffer}. + * + * @throws java.nio.ReadOnlyBufferException if the target is read-only + * @throws java.nio.BufferOverflowException if the target's {@link ByteBuffer#remaining()} space + * is not large enough to hold the data + */ + public final void copyTo(ByteBuffer target) { + byteString.copyTo(target); + } + + /** + * Copies the content of this {@code ByteArray} into an array of bytes. + * + * @throws IndexOutOfBoundsException if the target is not large enough to hold the data + */ + public final void copyTo(byte[] target) { + byteString.copyTo(target, 0); + } + + /** Creates a {@code ByteArray} object given an array of bytes. The bytes are copied. */ + public static final ByteArray copyFrom(byte[] bytes) { + return new ByteArray(ByteString.copyFrom(bytes)); + } + + /** + * Creates a {@code ByteArray} object given a string. The string is encoded in {@code UTF-8}. The + * bytes are copied. + */ + public static final ByteArray copyFrom(String string) { + return new ByteArray(ByteString.copyFrom(string, StandardCharsets.UTF_8)); + } + + /** Creates a {@code ByteArray} object given a {@link ByteBuffer}. The bytes are copied. */ + public static final ByteArray copyFrom(ByteBuffer bytes) { + return new ByteArray(ByteString.copyFrom(bytes)); + } + + /** + * Creates a {@code ByteArray} object given an {@link InputStream}. The stream is read into the + * created object. + */ + public static final ByteArray copyFrom(InputStream input) throws IOException { + return new ByteArray(ByteString.readFrom(input)); + } + + /** Creates a {@code ByteArray} from a base64 representation. */ + public static ByteArray fromBase64(String data) { + return ByteArray.copyFrom(encoder.decode(data)); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/Date.java b/google-cloud-core/src/main/java/com/google/cloud/Date.java new file mode 100644 index 0000000000..d7f3b71275 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/Date.java @@ -0,0 +1,159 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.api.core.BetaApi; +import com.google.common.base.Preconditions; +import java.io.Serializable; +import java.util.Calendar; +import java.util.Objects; + +/** Represents a Date without time, such as 2017-03-17. Date is timezone independent. */ +@BetaApi("This is going to be replaced with LocalDate from threetenbp") +public final class Date implements Comparable, Serializable { + + // Date format "yyyy-mm-dd" + private static final long serialVersionUID = 8067099123096783929L; + private final int year; + private final int month; + private final int dayOfMonth; + + private Date(int year, int month, int dayOfMonth) { + Preconditions.checkArgument(year > 0, "Invalid year: " + year); + Preconditions.checkArgument(month > 0 && month <= 12, "Invalid month: " + month); + Preconditions.checkArgument(dayOfMonth > 0 && dayOfMonth <= 31, "Invalid day: " + dayOfMonth); + this.year = year; + this.month = month; + this.dayOfMonth = dayOfMonth; + } + + /** + * Constructs a new Date instance. + * + * @param year must be greater than 0 + * @param month must be between [1,12] + * @param dayOfMonth must be between [1,31] + */ + public static Date fromYearMonthDay(int year, int month, int dayOfMonth) { + return new Date(year, month, dayOfMonth); + } + + /** @param date Data in RFC 3339 date format (yyyy-mm-dd). */ + public static Date parseDate(String date) { + Preconditions.checkNotNull(date); + final String invalidDate = "Invalid date: " + date; + Preconditions.checkArgument(date.length() == 10, invalidDate); + Preconditions.checkArgument(date.charAt(4) == '-', invalidDate); + Preconditions.checkArgument(date.charAt(7) == '-', invalidDate); + try { + int year = Integer.parseInt(date.substring(0, 4)); + int month = Integer.parseInt(date.substring(5, 7)); + int dayOfMonth = Integer.parseInt(date.substring(8, 10)); + return new Date(year, month, dayOfMonth); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(invalidDate, e); + } + } + + /** + * Convert a Google Date to a Java Util Date. + * + * @param date the date of the Google Date. + * @return java.util.Date + */ + public static java.util.Date toJavaUtilDate(Date date) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + // Calender.MONTH starts from 0 while G C date starts from 1 + cal.set(date.year, date.month - 1, date.dayOfMonth); + return cal.getTime(); + } + + /** + * Convert a Java Util Date to a Google Date. + * + * @param date the date of the java.util.Date + * @return Google Java Date + */ + public static Date fromJavaUtilDate(java.util.Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + // Calender.MONTH starts from 0 while G C date starts from 1 + return new Date( + cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)); + } + + /** Returns the year. */ + public int getYear() { + return year; + } + + /** Returns the month between 1 and 12 inclusive. */ + public int getMonth() { + return month; + } + + /** Returns day of month between 1 and 31 inclusive. */ + public int getDayOfMonth() { + return dayOfMonth; + } + + @Override + public String toString() { + return String.format("%04d-%02d-%02d", year, month, dayOfMonth); + } + + StringBuilder toString(StringBuilder b) { + return b.append(toString()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Date that = (Date) o; + return year == that.year && month == that.month && dayOfMonth == that.dayOfMonth; + } + + @Override + public int hashCode() { + return Objects.hash(year, month, dayOfMonth); + } + + @Override + public int compareTo(Date other) { + int r = Integer.compare(year, other.year); + if (r == 0) { + r = Integer.compare(month, other.month); + if (r == 0) { + r = Integer.compare(dayOfMonth, other.dayOfMonth); + } + } + return r; + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/ExceptionHandler.java b/google-cloud-core/src/main/java/com/google/cloud/ExceptionHandler.java new file mode 100644 index 0000000000..d243de1807 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/ExceptionHandler.java @@ -0,0 +1,296 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.core.BetaApi; +import com.google.api.gax.retrying.ResultRetryAlgorithm; +import com.google.api.gax.retrying.TimedAttemptSettings; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Callable; + +/** Exception retry algorithm implementation used by {@link RetryHelper}. */ +@BetaApi +public final class ExceptionHandler implements ResultRetryAlgorithm, Serializable { + + private static final long serialVersionUID = -2460707015779532919L; + + private static final ExceptionHandler DEFAULT_INSTANCE = + newBuilder().retryOn(Exception.class).abortOn(RuntimeException.class).build(); + + private final ImmutableList interceptors; + private final ImmutableSet> retriableExceptions; + private final ImmutableSet> nonRetriableExceptions; + private final Set retryInfo = Sets.newHashSet(); + + public interface Interceptor extends Serializable { + + enum RetryResult { + NO_RETRY, + RETRY, + CONTINUE_EVALUATION; + } + + /** + * 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#NO_RETRY}), or evaluation should + * proceed ({@link RetryResult#CONTINUE_EVALUATION}). + */ + 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#NO_RETRY}), or evaluation should + * proceed ({@link RetryResult#CONTINUE_EVALUATION}). + */ + RetryResult afterEval(Exception exception, RetryResult retryResult); + } + + /** ExceptionHandler builder. */ + public static class Builder { + + private final ImmutableList.Builder interceptors = ImmutableList.builder(); + private final ImmutableSet.Builder> retriableExceptions = + ImmutableSet.builder(); + private final ImmutableSet.Builder> nonRetriableExceptions = + ImmutableSet.builder(); + + private Builder() {} + + /** + * Adds the exception handler interceptors. Call order will be maintained. + * + * @param interceptors the interceptors for this exception handler + * @return the Builder for chaining + */ + public Builder addInterceptors(Interceptor... interceptors) { + for (Interceptor interceptor : interceptors) { + this.interceptors.add(interceptor); + } + return this; + } + + /** + * Add the exceptions to ignore/retry-on. + * + * @param exceptions retry should continue when such exceptions are thrown + * @return the Builder for chaining + */ + @SafeVarargs + public final Builder retryOn(Class... exceptions) { + for (Class exception : exceptions) { + retriableExceptions.add(checkNotNull(exception)); + } + return this; + } + + /** + * Adds the exceptions to abort on. + * + * @param exceptions retry should abort when such exceptions are thrown + * @return the Builder for chaining + */ + @SafeVarargs + public final Builder abortOn(Class... exceptions) { + for (Class exception : exceptions) { + nonRetriableExceptions.add(checkNotNull(exception)); + } + return this; + } + + /** Returns a new ExceptionHandler instance. */ + public ExceptionHandler build() { + return new ExceptionHandler(this); + } + } + + @VisibleForTesting + static final class RetryInfo implements Serializable { + + private static final long serialVersionUID = -4264634837841455974L; + private final Class exception; + private final Interceptor.RetryResult retry; + private final Set children = Sets.newHashSet(); + + RetryInfo(Class exception, Interceptor.RetryResult retry) { + this.exception = checkNotNull(exception); + this.retry = checkNotNull(retry); + } + + @Override + public int hashCode() { + return exception.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof RetryInfo)) { + return false; + } + // We only care about exception in equality as we allow only one instance per exception + return ((RetryInfo) obj).exception.equals(exception); + } + } + + private ExceptionHandler(Builder builder) { + interceptors = builder.interceptors.build(); + retriableExceptions = builder.retriableExceptions.build(); + nonRetriableExceptions = builder.nonRetriableExceptions.build(); + Preconditions.checkArgument( + Sets.intersection(retriableExceptions, nonRetriableExceptions).isEmpty(), + "Same exception was found in both retryable and non-retryable sets"); + for (Class exception : retriableExceptions) { + addRetryInfo(new RetryInfo(exception, Interceptor.RetryResult.RETRY), retryInfo); + } + for (Class exception : nonRetriableExceptions) { + addRetryInfo(new RetryInfo(exception, Interceptor.RetryResult.NO_RETRY), retryInfo); + } + } + + private static void addRetryInfo(RetryInfo retryInfo, Set dest) { + for (RetryInfo current : dest) { + if (current.exception.isAssignableFrom(retryInfo.exception)) { + addRetryInfo(retryInfo, current.children); + return; + } + if (retryInfo.exception.isAssignableFrom(current.exception)) { + retryInfo.children.add(current); + } + } + dest.removeAll(retryInfo.children); + dest.add(retryInfo); + } + + private static RetryInfo findMostSpecificRetryInfo( + Set retryInfo, Class exception) { + for (RetryInfo current : retryInfo) { + if (current.exception.isAssignableFrom(exception)) { + RetryInfo match = findMostSpecificRetryInfo(current.children, exception); + return match == null ? current : match; + } + } + return null; + } + + // called for Class, therefore a "call" method must be found. + private static Method getCallableMethod(Class clazz) { + try { + return clazz.getDeclaredMethod("call"); + } catch (NoSuchMethodException e) { + // check parent + return getCallableMethod(clazz.getSuperclass()); + } catch (SecurityException e) { + // This should never happen + throw new IllegalStateException("Unexpected exception", e); + } + } + + 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 derived from Exception"); + @SuppressWarnings("unchecked") + Class exception = (Class) exceptionOrError; + Preconditions.checkArgument( + findMostSpecificRetryInfo(retryInfo, exception) != null, + "Declared exception '" + exception + "' is not covered by exception handler"); + } + } + + @Override + public boolean shouldRetry(Throwable prevThrowable, Object prevResponse) { + if (!(prevThrowable instanceof Exception)) { + return false; + } + Exception ex = (Exception) prevThrowable; + for (Interceptor interceptor : interceptors) { + Interceptor.RetryResult retryResult = checkNotNull(interceptor.beforeEval(ex)); + if (retryResult != Interceptor.RetryResult.CONTINUE_EVALUATION) { + return retryResult == Interceptor.RetryResult.RETRY; + } + } + RetryInfo retryInfo = findMostSpecificRetryInfo(this.retryInfo, ex.getClass()); + Interceptor.RetryResult retryResult = + retryInfo == null ? Interceptor.RetryResult.NO_RETRY : retryInfo.retry; + for (Interceptor interceptor : interceptors) { + Interceptor.RetryResult interceptorRetry = + checkNotNull(interceptor.afterEval(ex, retryResult)); + if (interceptorRetry != Interceptor.RetryResult.CONTINUE_EVALUATION) { + retryResult = interceptorRetry; + } + } + return retryResult == Interceptor.RetryResult.RETRY; + } + + @Override + public TimedAttemptSettings createNextAttempt( + Throwable prevThrowable, Object prevResponse, TimedAttemptSettings prevSettings) { + // Return null to indicate that this implementation does not provide any specific attempt + // settings, so by default the TimedRetryAlgorithm options can be used instead. + return null; + } + + @Override + public int hashCode() { + return Objects.hash(interceptors, retriableExceptions, nonRetriableExceptions, retryInfo); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof ExceptionHandler)) { + return false; + } + ExceptionHandler other = (ExceptionHandler) obj; + return Objects.equals(interceptors, other.interceptors) + && Objects.equals(retriableExceptions, other.retriableExceptions) + && Objects.equals(nonRetriableExceptions, other.nonRetriableExceptions) + && Objects.equals(retryInfo, other.retryInfo); + } + + /** Returns an instance which retry any checked exception and abort on any runtime exception. */ + public static ExceptionHandler getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + public static Builder newBuilder() { + return new Builder(); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/FieldSelector.java b/google-cloud-core/src/main/java/com/google/cloud/FieldSelector.java new file mode 100644 index 0000000000..aad60f7a4d --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/FieldSelector.java @@ -0,0 +1,127 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.api.core.InternalApi; +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * Interface for Google Cloud resource's fields. Implementations of this interface can be used to + * select only desired fields from a returned Google Cloud resource. + */ +public interface FieldSelector { + + /** + * Returns a string selector. This selector is passed to a Google Cloud service (possibly with + * other field selectors) to specify which resource fields should be returned by an API call. + */ + String getSelector(); + + /** + * A helper class used to build composite selectors given a number of fields. This class is not + * supposed to be used directly by users. + */ + @InternalApi + class Helper { + + private static final String[] EMPTY_FIELDS = {}; + + private Helper() {} + + private static final Function FIELD_TO_STRING_FUNCTION = + new Function() { + @Override + public String apply(FieldSelector fieldSelector) { + return fieldSelector.getSelector(); + } + }; + + private static String selector( + List required, + FieldSelector[] others, + String... extraResourceFields) { + Set fieldStrings = Sets.newHashSetWithExpectedSize(required.size() + others.length); + fieldStrings.addAll(Lists.transform(required, FIELD_TO_STRING_FUNCTION)); + fieldStrings.addAll(Lists.transform(Arrays.asList(others), FIELD_TO_STRING_FUNCTION)); + fieldStrings.addAll(Arrays.asList(extraResourceFields)); + return Joiner.on(',').join(fieldStrings); + } + + /** + * Returns a composite selector given a number of resource fields. The string selector returned + * by this method can be used for field selection in API calls that return a single resource. + * This method is not supposed to be used directly by users. + */ + public static String selector(List required, FieldSelector... others) { + return selector(required, others, new String[] {}); + } + + /** + * Returns a composite selector given a number of resource fields and a container name. The + * string selector returned by this method can be used for field selection in API calls that + * return a list of resources. This method is not supposed to be used directly by users. + */ + public static String listSelector( + String containerName, List required, FieldSelector... others) { + return "nextPageToken," + containerName + '(' + selector(required, others) + ')'; + } + + /** + * Returns a composite selector given a number of resource fields and a container name. This + * method also takes an {@code extraResourceFields} parameter to specify some extra resource + * fields as strings. The string selector returned by this method can be used for field + * selection in API calls that return a list of resources. This method is not supposed to be + * used directly by users. + */ + public static String listSelector( + String containerName, + List required, + FieldSelector[] others, + String... extraResourceFields) { + return listSelector(EMPTY_FIELDS, containerName, required, others, extraResourceFields); + } + + /** + * Returns a composite selector given a number of top level fields as strings, a number of + * resource fields and a container name. This method also takes an {@code extraResourceFields} + * parameter to specify some extra resource fields as strings. The string selector returned by + * this method can be used for field selection in API calls that return a list of resources. + * This method is not supposed to be used directly by users. + */ + public static String listSelector( + String[] topLevelFields, + String containerName, + List required, + FieldSelector[] others, + String... extraResourceFields) { + Set topLevelStrings = Sets.newHashSetWithExpectedSize(topLevelFields.length + 1); + topLevelStrings.addAll(Lists.asList("nextPageToken", topLevelFields)); + return Joiner.on(',').join(topLevelStrings) + + "," + + containerName + + '(' + + selector(required, others, extraResourceFields) + + ')'; + } + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/GcpLaunchStage.java b/google-cloud-core/src/main/java/com/google/cloud/GcpLaunchStage.java new file mode 100644 index 0000000000..e83f773c9d --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/GcpLaunchStage.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A qualifier indicating what level of access and support can be expected of a particular Google + * Cloud Platform feature. The "GeneralAvailability" qualifier is not provided since the vast + * majority of features are in this category. Note that features may be in a later launch stage than + * the client library annotation indicates. + * + *

See more at the Launch Stages + * Documentation. + */ +public class GcpLaunchStage { + + /** + * Early Access features are limited to a closed group of testers. To use these features, you must + * sign up in advance and sign a Trusted Tester agreement (which includes confidentiality + * provisions). These features may be unstable, changed in backward-incompatible ways, and are not + * guaranteed to be released. + */ + @Retention(RetentionPolicy.RUNTIME) + public @interface EarlyAccess {} + + /** + * Alpha is a limited availability test for releases before they are cleared for widespread use. + * By Alpha, all significant design issues are resolved and we are in the process of verifying + * functionality. Alpha customers need to apply for access, agree to applicable terms, and have + * their projects whitelisted. Alpha releases don’t have to be feature complete, no SLAs are + * provided, and there are no technical support obligations, but they will be far enough along + * that customers can actually use them in test environments or for limited-use tests -- just like + * they would in normal production cases. + */ + @Retention(RetentionPolicy.RUNTIME) + public @interface Alpha {} + + /** + * Beta is the point at which we are ready to open a release for any customer to use. There are no + * SLA or technical support obligations in a Beta release, and charges may be waived in some + * cases. Products will be complete from a feature perspective, but may have some open outstanding + * issues. Beta releases are suitable for limited production use cases. + */ + @Retention(RetentionPolicy.RUNTIME) + public @interface Beta {} + + /** + * Deprecated features are scheduled to be shut down and removed. For more information, see the + * “Deprecation Policy” section of our Terms of + * Service and the Google Cloud Platform + * Subject to the Deprecation Policy documentation. + */ + @Retention(RetentionPolicy.RUNTIME) + public @interface Deprecated {} + + private GcpLaunchStage() { + // Intentionally left blank. + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/Identity.java b/google-cloud-core/src/main/java/com/google/cloud/Identity.java new file mode 100644 index 0000000000..a3aff1ade7 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/Identity.java @@ -0,0 +1,268 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.core.ApiFunction; +import com.google.common.base.CaseFormat; +import java.io.Serializable; +import java.util.Objects; + +/** + * An identity in a {@link Policy}. The following types of identities are permitted in IAM policies: + * + *

    + *
  • Google account + *
  • Service account + *
  • Google group + *
  • Google Apps domain + *
+ * + *

There are also two special identities that represent all users and all Google-authenticated + * accounts. + * + * @see Concepts + * related to identity + */ +public final class Identity implements Serializable { + + private static final long serialVersionUID = -8181841964597657446L; + + private final Type type; + private final String value; + + /** The types of IAM identities. */ + public static final class Type extends StringEnumValue { + private static final long serialVersionUID = 3809891273596003916L; + + private Type(String constant) { + super(constant); + } + + private static final ApiFunction CONSTRUCTOR = + new ApiFunction() { + @Override + public Type apply(String constant) { + return new Type(constant); + } + }; + + private static final StringEnumType type = new StringEnumType(Type.class, CONSTRUCTOR); + + /** Represents anyone who is on the internet; with or without a Google account. */ + public static final Type ALL_USERS = type.createAndRegister("ALL_USERS"); + + /** Represents anyone who is authenticated with a Google account or a service account. */ + public static final Type ALL_AUTHENTICATED_USERS = + type.createAndRegister("ALL_AUTHENTICATED_USERS"); + + /** Represents a specific Google account. */ + public static final Type USER = type.createAndRegister("USER"); + + /** Represents a service account. */ + public static final Type SERVICE_ACCOUNT = type.createAndRegister("SERVICE_ACCOUNT"); + + /** Represents a Google group. */ + public static final Type GROUP = type.createAndRegister("GROUP"); + + /** Represents all the users of a Google Apps domain name. */ + public static final Type DOMAIN = type.createAndRegister("DOMAIN"); + + /** Represents owners of a Google Cloud Platform project. */ + public static final Type PROJECT_OWNER = type.createAndRegister("PROJECT_OWNER"); + + /** Represents editors of a Google Cloud Platform project. */ + public static final Type PROJECT_EDITOR = type.createAndRegister("PROJECT_EDITOR"); + + /** Represents viewers of a Google Cloud Platform project. */ + public static final Type PROJECT_VIEWER = type.createAndRegister("PROJECT_VIEWER"); + + /** + * Get the Type for the given String constant, and throw an exception if the constant is not + * recognized. + */ + public static Type valueOfStrict(String constant) { + return type.valueOfStrict(constant); + } + + /** Get the Type for the given String constant, and allow unrecognized values. */ + public static Type valueOf(String constant) { + return type.valueOf(constant); + } + + /** Return the known values for Type. */ + public static Type[] values() { + return type.values(); + } + } + + private Identity(Type type, String value) { + this.type = type; + this.value = value; + } + + public Type getType() { + return type; + } + + /** + * Returns the string identifier for this identity. The value corresponds to: + * + *

    + *
  • email address (for identities of type {@code USER}, {@code SERVICE_ACCOUNT}, and {@code + * GROUP}) + *
  • domain (for identities of type {@code DOMAIN}) + *
  • {@code null} (for identities of type {@code ALL_USERS} and {@code + * ALL_AUTHENTICATED_USERS}) + *
+ */ + public String getValue() { + return value; + } + + /** + * Returns a new identity representing anyone who is on the internet; with or without a Google + * account. + */ + public static Identity allUsers() { + return new Identity(Type.ALL_USERS, null); + } + + /** + * Returns a new identity representing anyone who is authenticated with a Google account or a + * service account. + */ + public static Identity allAuthenticatedUsers() { + return new Identity(Type.ALL_AUTHENTICATED_USERS, null); + } + + /** + * Returns a new user identity. + * + * @param email An email address that represents a specific Google account. For example, + * alice@gmail.com or joe@example.com. + */ + public static Identity user(String email) { + return new Identity(Type.USER, checkNotNull(email)); + } + + /** + * Returns a new service account identity. + * + * @param email An email address that represents a service account. For example, + * my-other-app@appspot.gserviceaccount.com. + */ + public static Identity serviceAccount(String email) { + return new Identity(Type.SERVICE_ACCOUNT, checkNotNull(email)); + } + + /** + * Returns a new group identity. + * + * @param email An email address that represents a Google group. For example, + * admins@example.com. + */ + public static Identity group(String email) { + return new Identity(Type.GROUP, checkNotNull(email)); + } + + /** + * Returns a new domain identity. + * + * @param domain A Google Apps domain name that represents all the users of that domain. For + * example, google.com or example.com. + */ + public static Identity domain(String domain) { + return new Identity(Type.DOMAIN, checkNotNull(domain)); + } + + /** + * Returns a new project owner identity. + * + * @param projectId A Google Cloud Platform project ID. For example, my-sample-project. + */ + public static Identity projectOwner(String projectId) { + return new Identity(Type.PROJECT_OWNER, checkNotNull(projectId)); + } + + /** + * Returns a new project editor identity. + * + * @param projectId A Google Cloud Platform project ID. For example, my-sample-project. + */ + public static Identity projectEditor(String projectId) { + return new Identity(Type.PROJECT_EDITOR, checkNotNull(projectId)); + } + + /** + * Returns a new project viewer identity. + * + * @param projectId A Google Cloud Platform project ID. For example, my-sample-project. + */ + public static Identity projectViewer(String projectId) { + return new Identity(Type.PROJECT_VIEWER, checkNotNull(projectId)); + } + + @Override + public String toString() { + return strValue(); + } + + @Override + public int hashCode() { + return Objects.hash(value, type); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Identity)) { + return false; + } + Identity other = (Identity) obj; + return Objects.equals(value, other.getValue()) && Objects.equals(type, other.getType()); + } + + /** + * Returns the string value associated with the identity. Used primarily for converting from + * {@code Identity} objects to strings for protobuf-generated policies. + */ + public String strValue() { + String protobufString = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, type.toString()); + if (value == null) { + return protobufString; + } else { + return protobufString + ":" + value; + } + } + + /** + * Converts a string to an {@code Identity}. Used primarily for converting protobuf-generated + * policy identities to {@code Identity} objects. + */ + public static Identity valueOf(String identityStr) { + String[] info = identityStr.split(":", 2); + Type type = Type.valueOf(CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, info[0])); + if (info.length == 1) { + return new Identity(type, null); + } else if (info.length == 2) { + return new Identity(type, info[1]); + } else { + throw new IllegalArgumentException("Illegal identity string: \"" + identityStr + "\""); + } + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/MetadataConfig.java b/google-cloud-core/src/main/java/com/google/cloud/MetadataConfig.java new file mode 100644 index 0000000000..8f1bac04ca --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/MetadataConfig.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * Retrieves Google Cloud project-id and a limited set of instance attributes from Metadata server. + * + * @see + * https://cloud.google.com/compute/docs/storing-retrieving-metadata + */ +public class MetadataConfig { + + private static final String METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/"; + private static final int TIMEOUT_MS = 5000; + + private MetadataConfig() {} + + public static String getProjectId() { + return getAttribute("project/project-id"); + } + + public static String getZone() { + String zoneId = getAttribute("instance/zone"); + if (zoneId != null && zoneId.contains("/")) { + return zoneId.substring(zoneId.lastIndexOf('/') + 1); + } + return zoneId; + } + + public static String getInstanceId() { + return getAttribute("instance/id"); + } + + public static String getClusterName() { + return getAttribute("instance/attributes/cluster-name"); + } + + public static String getContainerName() { + return getAttribute("instance/attributes/container-name"); + } + + public static String getNamespaceId() { + return getAttribute("instance/attributes/namespace-id"); + } + + public static String getAttribute(String attributeName) { + try { + URL url = new URL(METADATA_URL + attributeName); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(TIMEOUT_MS); + connection.setReadTimeout(TIMEOUT_MS); + connection.setRequestProperty("Metadata-Flavor", "Google"); + try (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 + } + return null; + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/MonitoredResource.java b/google-cloud-core/src/main/java/com/google/cloud/MonitoredResource.java new file mode 100644 index 0000000000..a1fbe44b92 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/MonitoredResource.java @@ -0,0 +1,165 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Objects of this class represent a resource that can be used for monitoring, logging, billing, or + * other purposes. Examples include virtual machine instances, databases, and storage devices such + * as disks. The type field identifies a {@link MonitoredResourceDescriptor} object that describes + * the resource's schema. Information in the labels field identifies the actual resource and its + * attributes according to the schema. + * + *

For example, the monitored resource for Google Compute Engine VM instances has {@code + * gce_instance} type and specifies values for the labels {@code instance_id} and {@code zone} to + * identify particular VM instances. + */ +public final class MonitoredResource implements Serializable { + + private static final long serialVersionUID = -4393604148752640581L; + + private final String type; + private final Map labels; + + /** A builder for {@code MonitoredResource} objects. */ + public static class Builder { + + private String type; + private Map labels = new HashMap<>(); + + Builder(String type) { + this.type = type; + } + + Builder(MonitoredResource monitoredResource) { + this.type = monitoredResource.type; + this.labels = new HashMap<>(monitoredResource.labels); + } + + /** + * Sets the monitored resource type. This value must match the one of {@link + * MonitoredResourceDescriptor#getType()} of a {@code MonitoredResourceDescriptor} object. For + * example, the type {@code cloudsql_database} represent databases in Google Cloud SQL. + */ + public Builder setType(String type) { + this.type = type; + return this; + } + + /** + * Sets the values for all the labels required by the corresponding monitored resource + * descriptor (see {@link MonitoredResourceDescriptor#getLabels()}. For example, Google Compute + * Engine VM instances use the labels {@code instance_id} and {@code zone}. + */ + public Builder setLabels(Map labels) { + this.labels = new HashMap<>(checkNotNull(labels)); + return this; + } + + /** Adds a label to the labels of the monitored resource. */ + public Builder addLabel(String key, String value) { + this.labels.put(key, value); + return this; + } + + /** Clears all the labels of the monitored resource. */ + public Builder clearLabels() { + this.labels.clear(); + return this; + } + + public MonitoredResource build() { + return new MonitoredResource(this); + } + } + + MonitoredResource(Builder builder) { + this.type = checkNotNull(builder.type); + this.labels = ImmutableMap.copyOf(builder.labels); + } + + /** + * Returns the monitored resource type. This value must match the one of {@link + * MonitoredResourceDescriptor#getType()} of a {@code MonitoredResourceDescriptor} object. For + * example, the type {@code cloudsql_database} represent databases in Google Cloud SQL. + */ + public String getType() { + return type; + } + + /** + * Returns the values for all the labels required by the corresponding monitored resource + * descriptor (see {@link MonitoredResourceDescriptor#getLabels()}. For example, Google Compute + * Engine VM instances use the labels {@code instance_id} and {@code zone}. + */ + public Map getLabels() { + return labels; + } + + @Override + public int hashCode() { + return Objects.hash(type, labels); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof MonitoredResource)) { + return false; + } + MonitoredResource other = (MonitoredResource) obj; + return Objects.equals(type, other.type) && Objects.equals(labels, other.labels); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("type", type).add("labels", labels).toString(); + } + + public com.google.api.MonitoredResource toPb() { + return com.google.api.MonitoredResource.newBuilder().setType(type).putAllLabels(labels).build(); + } + + /** Returns a builder for this {@code MonitoredResource} object. */ + public Builder toBuilder() { + return new Builder(this); + } + + /** Returns a builder for {@code MonitoredResource} objects given the resource's type. */ + public static Builder newBuilder(String type) { + return new Builder(type); + } + + /** Creates a {@code MonitoredResource} object given the resource's type and labels. */ + public static MonitoredResource of(String type, Map labels) { + return newBuilder(type).setLabels(labels).build(); + } + + public static MonitoredResource fromPb(com.google.api.MonitoredResource descriptorPb) { + return new Builder(descriptorPb.getType()).setLabels(descriptorPb.getLabelsMap()).build(); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/MonitoredResourceDescriptor.java b/google-cloud-core/src/main/java/com/google/cloud/MonitoredResourceDescriptor.java new file mode 100644 index 0000000000..cfc24590d4 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/MonitoredResourceDescriptor.java @@ -0,0 +1,342 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.core.ApiFunction; +import com.google.common.base.Function; +import com.google.common.base.MoreObjects; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * This class describes the schema of Cloud monitored resources. Monitored resource descriptors + * contain a type name and a set of labels. For example, the monitored resource descriptor for + * Google Compute Engine VM instances has a type of {@code gce_instance} and specifies the use of + * the labels {@code instance_id} and {@code zone} to identify particular VM instances. + */ +public class MonitoredResourceDescriptor implements Serializable { + + private static final long serialVersionUID = -3702077512777687441L; + public static final ApiFunction< + com.google.api.MonitoredResourceDescriptor, MonitoredResourceDescriptor> + FROM_PB_FUNCTION = + new ApiFunction< + com.google.api.MonitoredResourceDescriptor, MonitoredResourceDescriptor>() { + @Override + public MonitoredResourceDescriptor apply( + com.google.api.MonitoredResourceDescriptor pb) { + return fromPb(pb); + } + }; + + private final String type; + private final String name; + private final String displayName; + private final String description; + private final List labels; + + /** + * This class describes a label for a monitored resource. Label descriptors contain the key for + * the label, the type of data that the label can hold and an optional description. + */ + public static class LabelDescriptor implements Serializable { + + private static final long serialVersionUID = -2811608103754481777L; + private static final Function + FROM_PB_FUNCTION = + new Function() { + @Override + public LabelDescriptor apply(com.google.api.LabelDescriptor descriptorPb) { + return fromPb(descriptorPb); + } + }; + private static final Function TO_PB_FUNCTION = + new Function() { + @Override + public com.google.api.LabelDescriptor apply(LabelDescriptor descriptor) { + return descriptor.toPb(); + } + }; + + private final String key; + private final ValueType valueType; + private final String description; + + /** Value types that can be used as label values. */ + public enum ValueType { + STRING(com.google.api.LabelDescriptor.ValueType.STRING), + BOOL(com.google.api.LabelDescriptor.ValueType.BOOL), + INT64(com.google.api.LabelDescriptor.ValueType.INT64); + + private com.google.api.LabelDescriptor.ValueType typePb; + + ValueType(com.google.api.LabelDescriptor.ValueType typePb) { + this.typePb = typePb; + } + + com.google.api.LabelDescriptor.ValueType toPb() { + return typePb; + } + + static ValueType fromPb(com.google.api.LabelDescriptor.ValueType typePb) { + switch (typePb) { + case STRING: + return ValueType.STRING; + case BOOL: + return ValueType.BOOL; + case INT64: + return ValueType.INT64; + default: + throw new IllegalArgumentException("Unrecognized label type"); + } + } + } + + LabelDescriptor(String key, ValueType valueType, String description) { + this.key = checkNotNull(key); + this.valueType = checkNotNull(valueType); + this.description = description; + } + + /** Returns the key associated to this label. */ + public String getKey() { + return key; + } + + /** Returns the type of data that can be assigned to this label. */ + public ValueType getValueType() { + return valueType; + } + + /** + * Returns the optional human-readable description for this label. If not set, this method + * returns {@code null}. + */ + public String getDescription() { + return description; + } + + @Override + public final int hashCode() { + return Objects.hash(key, valueType, description); + } + + @Override + public final boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || !obj.getClass().equals(LabelDescriptor.class)) { + return false; + } + LabelDescriptor other = (LabelDescriptor) obj; + return Objects.equals(key, other.key) + && Objects.equals(valueType, other.valueType) + && Objects.equals(description, other.description); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("key", key) + .add("valueType", valueType) + .add("description", description) + .toString(); + } + + com.google.api.LabelDescriptor toPb() { + com.google.api.LabelDescriptor.Builder builder = + com.google.api.LabelDescriptor.newBuilder().setKey(key).setValueType(valueType.toPb()); + if (description != null) { + builder.setDescription(description); + } + return builder.build(); + } + + static LabelDescriptor fromPb(com.google.api.LabelDescriptor descriptorPb) { + String description = null; + if (descriptorPb.getDescription() != null && !descriptorPb.getDescription().equals("")) { + description = descriptorPb.getDescription(); + } + return new LabelDescriptor( + descriptorPb.getKey(), ValueType.fromPb(descriptorPb.getValueType()), description); + } + } + + static class Builder { + + private final String type; + private String name; + private String displayName; + private String description; + private List labels = new ArrayList<>(); + + Builder(String type) { + this.type = type; + } + + Builder setName(String name) { + this.name = name; + return this; + } + + Builder setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + Builder setDescription(String description) { + this.description = description; + return this; + } + + Builder setLabels(List labels) { + this.labels = labels; + return this; + } + + MonitoredResourceDescriptor build() { + return new MonitoredResourceDescriptor(this); + } + } + + MonitoredResourceDescriptor(Builder builder) { + this.type = checkNotNull(builder.type); + this.name = builder.name; + this.displayName = builder.displayName; + this.description = builder.description; + this.labels = checkNotNull(builder.labels); + } + + /** + * Returns the monitored resource type. For example, the type {@code cloudsql_database} represents + * databases in Google Cloud SQL. + */ + public String getType() { + return type; + } + + /** + * Returns an optional name for the monitored resource descriptor. If not set, this method returns + * {@code null}. + */ + public String getName() { + return name; + } + + /** + * Returns an optional concise name for the monitored resource type. This value might be displayed + * in user interfaces. For example, {@code Google Cloud SQL Database}. If not set, this method + * returns {@code null}. + */ + public String getDisplayName() { + return displayName; + } + + /** + * Returns an optional detailed description of the monitored resource type. This value might be + * used in documentation. If not set, this method returns {@code null}. + */ + public String getDescription() { + return description; + } + + /** + * Returns a list of labels used to describe instances of this monitored resource type. For + * example, an individual Google Cloud SQL database is identified by values for the labels {@code + * database_id} and {@code region}. + */ + public List getLabels() { + return labels; + } + + @Override + public final int hashCode() { + return Objects.hash(type, name, displayName, description, labels); + } + + @Override + public final boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || !obj.getClass().equals(MonitoredResourceDescriptor.class)) { + return false; + } + MonitoredResourceDescriptor other = (MonitoredResourceDescriptor) obj; + return Objects.equals(type, other.type) + && Objects.equals(name, other.name) + && Objects.equals(displayName, other.displayName) + && Objects.equals(description, other.description) + && Objects.equals(labels, other.labels); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("type", type) + .add("name", name) + .add("displayName", displayName) + .add("description", description) + .add("labels", labels) + .toString(); + } + + public com.google.api.MonitoredResourceDescriptor toPb() { + com.google.api.MonitoredResourceDescriptor.Builder builder = + com.google.api.MonitoredResourceDescriptor.newBuilder() + .setType(type) + .addAllLabels(Iterables.transform(labels, LabelDescriptor.TO_PB_FUNCTION)); + if (name != null) { + builder.setName(name); + } + if (displayName != null) { + builder.setDisplayName(displayName); + } + if (description != null) { + builder.setDescription(description); + } + return builder.build(); + } + + static Builder newBuilder(String type) { + return new Builder(type); + } + + public static MonitoredResourceDescriptor fromPb( + com.google.api.MonitoredResourceDescriptor descriptorPb) { + Builder builder = newBuilder(descriptorPb.getType()); + if (descriptorPb.getName() != null && !descriptorPb.getName().equals("")) { + builder.setName(descriptorPb.getName()); + } + if (descriptorPb.getDisplayName() != null && !descriptorPb.getDisplayName().equals("")) { + builder.setDisplayName(descriptorPb.getDisplayName()); + } + if (descriptorPb.getDescription() != null && !descriptorPb.getDescription().equals("")) { + builder.setDescription(descriptorPb.getDescription()); + } + builder.setLabels( + Lists.transform(descriptorPb.getLabelsList(), LabelDescriptor.FROM_PB_FUNCTION)); + return builder.build(); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/NoCredentials.java b/google-cloud-core/src/main/java/com/google/cloud/NoCredentials.java new file mode 100644 index 0000000000..4726c4d0c6 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/NoCredentials.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.auth.oauth2.OAuth2Credentials; +import java.io.ObjectStreamException; + +/** + * A placeholder for credentials to signify that requests sent to the server should not be + * authenticated. This is typically useful when using local service emulators. + */ +public class NoCredentials extends OAuth2Credentials { + + private static final long serialVersionUID = -6263971603971044288L; + private static final NoCredentials INSTANCE = new NoCredentials(); + + private NoCredentials() {} + + private Object readResolve() throws ObjectStreamException { + return INSTANCE; + } + + public static NoCredentials getInstance() { + return INSTANCE; + } + + @Override + public boolean equals(Object obj) { + return this == obj; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/PageImpl.java b/google-cloud-core/src/main/java/com/google/cloud/PageImpl.java new file mode 100644 index 0000000000..72e0e858b4 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/PageImpl.java @@ -0,0 +1,156 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.api.core.InternalApi; +import com.google.api.gax.paging.Page; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableMap; +import java.io.Serializable; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +/** + * Base implementation for Google Cloud paginated results. + * + * @param the value type that the page holds + */ +@InternalApi +public class PageImpl implements Page, Serializable { + + private static final long serialVersionUID = 3914827379823557934L; + + private final String cursor; + private final Iterable results; + private final NextPageFetcher pageFetcher; + + /** + * Interface for fetching the next page of results from the service. + * + * @param the value type that the page holds + */ + public interface NextPageFetcher extends Serializable { + + Page getNextPage(); + } + + static class PageIterator extends AbstractIterator { + + private Iterator currentPageIterator; + private Page currentPage; + + PageIterator(Page currentPage) { + this.currentPageIterator = currentPage.getValues().iterator(); + this.currentPage = currentPage; + } + + @Override + protected T computeNext() { + while (!currentPageIterator.hasNext()) { + currentPage = currentPage.getNextPage(); + if (currentPage == null) { + return endOfData(); + } + currentPageIterator = currentPage.getValues().iterator(); + } + return currentPageIterator.next(); + } + } + + /** + * Creates a {@code PageImpl} object. In order for the object to be serializable the {@code + * results} parameter must be serializable. + */ + public PageImpl(NextPageFetcher pageFetcher, String cursor, Iterable results) { + this.pageFetcher = pageFetcher; + this.cursor = cursor; + this.results = results; + } + + @Override + public Iterable getValues() { + return results == null ? Collections.emptyList() : results; + } + + @Override + public Iterable iterateAll() { + return new Iterable() { + @Override + public Iterator iterator() { + return new PageIterator<>(PageImpl.this); + } + }; + } + + @Override + public boolean hasNextPage() { + return getNextPageToken() != null && !getNextPageToken().equals(""); + } + + @Override + public String getNextPageToken() { + return cursor; + } + + @Override + public Page getNextPage() { + if (cursor == null || pageFetcher == null) { + return null; + } + return pageFetcher.getNextPage(); + } + + @Override + public int hashCode() { + return Objects.hash(cursor, results); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PageImpl)) { + return false; + } + PageImpl other = (PageImpl) obj; + return Objects.equals(cursor, other.cursor) && Objects.equals(results, other.results); + } + + /** + * Utility method to construct the options map for the next page request. + * + * @param the value type that the page holds. Instances of {@code T} should be {@code + * Serializable} + * @param pageTokenOption the key for the next page cursor option in the options map + * @param cursor the cursor for the next page + * @param optionMap the previous options map + * @return the options map for the next page request + */ + public static Map nextRequestOptions( + T pageTokenOption, String cursor, Map optionMap) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + if (cursor != null) { + builder.put(pageTokenOption, cursor); + } + for (Map.Entry option : optionMap.entrySet()) { + if (!Objects.equals(option.getKey(), pageTokenOption)) { + builder.put(option.getKey(), option.getValue()); + } + } + return builder.build(); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/PlatformInformation.java b/google-cloud-core/src/main/java/com/google/cloud/PlatformInformation.java new file mode 100644 index 0000000000..8c77bb9e91 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/PlatformInformation.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.api.core.InternalApi; + +@InternalApi("This class should only be used within google-cloud-java") +public final class PlatformInformation { + public static final String GAE_RUNTIME = System.getenv("GAE_RUNTIME"); + + private PlatformInformation() {} + + public static boolean isOnGAEStandard7() { + return "java7".equals(GAE_RUNTIME); + } + + public static boolean isOnGAEStandard8() { + return "java8".equals(GAE_RUNTIME); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/Policy.java b/google-cloud-core/src/main/java/com/google/cloud/Policy.java new file mode 100644 index 0000000000..84067ec87e --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/Policy.java @@ -0,0 +1,329 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.core.ApiFunction; +import com.google.api.core.InternalApi; +import com.google.common.base.Function; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.io.BaseEncoding; +import com.google.protobuf.ByteString; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Class for Identity and Access Management (IAM) policies. IAM policies are used to specify access + * settings for Cloud Platform resources. A policy is a map of bindings. A binding assigns a set of + * identities to a role, where the identities can be user accounts, Google groups, Google domains, + * and service accounts. A role is a named list of permissions defined by IAM. + * + * @see Policy + */ +public final class Policy implements Serializable { + + private static final long serialVersionUID = -3348914530232544290L; + + private final Map> bindings; + private final String etag; + private final int version; + + public abstract static class Marshaller { + + @InternalApi("This class should only be extended within google-cloud-java") + protected Marshaller() {} + + protected static final ApiFunction IDENTITY_VALUE_OF_FUNCTION = + new ApiFunction() { + @Override + public Identity apply(String identityPb) { + return Identity.valueOf(identityPb); + } + }; + protected static final ApiFunction IDENTITY_STR_VALUE_FUNCTION = + new ApiFunction() { + @Override + public String apply(Identity identity) { + return identity.strValue(); + } + }; + + protected abstract Policy fromPb(T policyPb); + + protected abstract T toPb(Policy policy); + } + + public static class DefaultMarshaller extends Marshaller { + + @Override + protected Policy fromPb(com.google.iam.v1.Policy policyPb) { + Map> bindings = new HashMap<>(); + for (com.google.iam.v1.Binding bindingPb : policyPb.getBindingsList()) { + bindings.put( + Role.of(bindingPb.getRole()), + ImmutableSet.copyOf( + Lists.transform( + bindingPb.getMembersList(), + new Function() { + @Override + public Identity apply(String s) { + return IDENTITY_VALUE_OF_FUNCTION.apply(s); + } + }))); + } + return newBuilder() + .setBindings(bindings) + .setEtag( + policyPb.getEtag().isEmpty() + ? null + : BaseEncoding.base64().encode(policyPb.getEtag().toByteArray())) + .setVersion(policyPb.getVersion()) + .build(); + } + + @Override + protected com.google.iam.v1.Policy toPb(Policy policy) { + com.google.iam.v1.Policy.Builder policyBuilder = com.google.iam.v1.Policy.newBuilder(); + List bindingPbList = new LinkedList<>(); + for (Map.Entry> binding : policy.getBindings().entrySet()) { + com.google.iam.v1.Binding.Builder bindingBuilder = com.google.iam.v1.Binding.newBuilder(); + bindingBuilder.setRole(binding.getKey().getValue()); + bindingBuilder.addAllMembers( + Lists.transform( + new ArrayList<>(binding.getValue()), + new Function() { + @Override + public String apply(Identity identity) { + return IDENTITY_STR_VALUE_FUNCTION.apply(identity); + } + })); + bindingPbList.add(bindingBuilder.build()); + } + policyBuilder.addAllBindings(bindingPbList); + if (policy.etag != null) { + policyBuilder.setEtag(ByteString.copyFrom(BaseEncoding.base64().decode(policy.etag))); + } + policyBuilder.setVersion(policy.version); + return policyBuilder.build(); + } + } + + /** A builder for {@code Policy} objects. */ + public static class Builder { + + private final Map> bindings = new HashMap<>(); + private String etag; + private int version; + + @InternalApi("This class should only be extended within google-cloud-java") + protected Builder() {} + + @InternalApi("This class should only be extended within google-cloud-java") + protected Builder(Policy policy) { + setBindings(policy.bindings); + setEtag(policy.etag); + setVersion(policy.version); + } + + /** + * Replaces the builder's map of bindings with the given map of bindings. + * + * @throws NullPointerException if the given map is null or contains any null keys or values + * @throws IllegalArgumentException if any identities in the given map are null + */ + public final Builder setBindings(Map> bindings) { + checkNotNull(bindings, "The provided map of bindings cannot be null."); + for (Map.Entry> binding : bindings.entrySet()) { + checkNotNull(binding.getKey(), "The role cannot be null."); + Set identities = binding.getValue(); + checkNotNull(identities, "A role cannot be assigned to a null set of identities."); + checkArgument(!identities.contains(null), "Null identities are not permitted."); + } + this.bindings.clear(); + for (Map.Entry> binding : bindings.entrySet()) { + this.bindings.put(binding.getKey(), new HashSet<>(binding.getValue())); + } + return this; + } + + /** Removes the role (and all identities associated with that role) from the policy. */ + public final Builder removeRole(Role role) { + bindings.remove(role); + return this; + } + + /** + * Adds one or more identities to the policy under the role specified. + * + * @throws NullPointerException if the role or any of the identities is null. + */ + public final Builder addIdentity(Role role, Identity first, Identity... others) { + String nullIdentityMessage = "Null identities are not permitted."; + checkNotNull(first, nullIdentityMessage); + checkNotNull(others, nullIdentityMessage); + for (Identity identity : others) { + checkNotNull(identity, nullIdentityMessage); + } + Set toAdd = new LinkedHashSet<>(); + toAdd.add(first); + toAdd.addAll(Arrays.asList(others)); + Set identities = bindings.get(checkNotNull(role, "The role cannot be null.")); + if (identities == null) { + identities = new HashSet<>(); + bindings.put(role, identities); + } + identities.addAll(toAdd); + return this; + } + + /** + * Removes one or more identities from an existing binding. Does nothing if the binding + * associated with the provided role doesn't exist. + */ + public final Builder removeIdentity(Role role, Identity first, Identity... others) { + Set identities = bindings.get(role); + if (identities != null) { + identities.remove(first); + identities.removeAll(Arrays.asList(others)); + } + if (identities != null && identities.isEmpty()) { + bindings.remove(role); + } + return this; + } + + /** + * Sets the policy's etag. + * + *

Etags are used for optimistic concurrency control as a way to help prevent simultaneous + * updates of a policy from overwriting each other. It is strongly suggested that systems make + * use of the etag in the read-modify-write cycle to perform policy updates in order to avoid + * race conditions. An etag is returned in the response to getIamPolicy, and systems are + * expected to put that etag in the request to setIamPolicy to ensure that their change will be + * applied to the same version of the policy. If no etag is provided in the call to + * setIamPolicy, then the existing policy is overwritten blindly. + */ + public final Builder setEtag(String etag) { + this.etag = etag; + return this; + } + + /** + * Sets the version of the policy. The default version is 0, meaning only the "owner", "editor", + * and "viewer" roles are permitted. If the version is 1, you may also use other roles. + */ + protected final Builder setVersion(int version) { + this.version = version; + return this; + } + + /** Creates a {@code Policy} object. */ + public final Policy build() { + return new Policy(this); + } + } + + private Policy(Builder builder) { + ImmutableMap.Builder> bindingsBuilder = ImmutableMap.builder(); + for (Map.Entry> binding : builder.bindings.entrySet()) { + bindingsBuilder.put(binding.getKey(), ImmutableSet.copyOf(binding.getValue())); + } + this.bindings = bindingsBuilder.build(); + this.etag = builder.etag; + this.version = builder.version; + } + + /** Returns a builder containing the properties of this IAM Policy. */ + public Builder toBuilder() { + return new Builder(this); + } + + /** Returns the map of bindings that comprises the policy. */ + public Map> getBindings() { + return bindings; + } + + /** + * Returns the policy's etag. + * + *

Etags are used for optimistic concurrency control as a way to help prevent simultaneous + * updates of a policy from overwriting each other. It is strongly suggested that systems make use + * of the etag in the read-modify-write cycle to perform policy updates in order to avoid race + * conditions. An etag is returned in the response to getIamPolicy, and systems are expected to + * put that etag in the request to setIamPolicy to ensure that their change will be applied to the + * same version of the policy. If no etag is provided in the call to setIamPolicy, then the + * existing policy is overwritten blindly. + */ + public String getEtag() { + return etag; + } + + /** + * Returns the version of the policy. The default version is 0, meaning only the "owner", + * "editor", and "viewer" roles are permitted. If the version is 1, you may also use other roles. + */ + public int getVersion() { + return version; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("bindings", bindings) + .add("etag", etag) + .add("version", version) + .toString(); + } + + @Override + public int hashCode() { + return Objects.hash(getClass(), bindings, etag, version); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Policy)) { + return false; + } + Policy other = (Policy) obj; + return Objects.equals(bindings, other.getBindings()) + && Objects.equals(etag, other.getEtag()) + && Objects.equals(version, other.getVersion()); + } + + /** Returns a builder for {@code Policy} objects. */ + public static Builder newBuilder() { + return new Builder(); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/ReadChannel.java b/google-cloud-core/src/main/java/com/google/cloud/ReadChannel.java new file mode 100644 index 0000000000..e0a8f5f50b --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/ReadChannel.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; + +/** + * A channel for reading data from a Google Cloud object. + * + *

Implementations of this class may buffer data internally to reduce remote calls. This + * interface implements {@link Restorable} to allow saving the reader's state to continue reading + * afterwards. + */ +public interface ReadChannel extends ReadableByteChannel, Closeable, Restorable { + + /** + * Overridden to remove IOException. + * + * @see java.nio.channels.Channel#close() + */ + @Override + void close(); + + void seek(long position) throws IOException; + + /** + * Sets the minimum size that will be read by a single RPC. Read data will be locally buffered + * until consumed. + */ + void setChunkSize(int chunkSize); + + /** + * Captures the read channel state so that it can be saved and restored afterwards. + * + * @return a {@link RestorableState} object that contains the read channel state and can restore + * it afterwards. + */ + @Override + RestorableState capture(); +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/Restorable.java b/google-cloud-core/src/main/java/com/google/cloud/Restorable.java new file mode 100644 index 0000000000..7feedf0c6e --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/Restorable.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +/** + * Implementation of this interface can persist their state and restore from it. + * + *

A typical capture usage: + * + *

{@code
+ * X restorableObj; // X instanceof Restorable
+ * RestorableState state = restorableObj.capture();
+ * .. persist state
+ * }
+ * + *

A typical restore usage: + * + *

{@code
+ * RestorableState state = ... // read from persistence
+ * X restorableObj = state.restore();
+ * ...
+ * }
+ * + * @param the restorable object's type + */ +public interface Restorable> { + + /** + * Captures the state of this object. + * + * @return a {@link RestorableState} instance that contains the state for this object and can + * restore it afterwards. + */ + RestorableState capture(); +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/RestorableState.java b/google-cloud-core/src/main/java/com/google/cloud/RestorableState.java new file mode 100644 index 0000000000..53a6458f1f --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/RestorableState.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +/** + * A common interface for restorable states. Implementations of {@code RestorableState} are capable + * of saving the state of an object to restore it for later use. + * + *

Implementations of this class must implement {@link java.io.Serializable} to ensure that the + * state of a the object can be correctly serialized. + * + * @param the restored object's type + */ +public interface RestorableState> { + + /** Returns an object whose internal state reflects the one saved in the invocation object. */ + T restore(); +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/RetryHelper.java b/google-cloud-core/src/main/java/com/google/cloud/RetryHelper.java new file mode 100644 index 0000000000..986fd35587 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/RetryHelper.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.api.core.ApiClock; +import com.google.api.core.BetaApi; +import com.google.api.gax.retrying.DirectRetryingExecutor; +import com.google.api.gax.retrying.ExponentialPollAlgorithm; +import com.google.api.gax.retrying.ExponentialRetryAlgorithm; +import com.google.api.gax.retrying.ResultRetryAlgorithm; +import com.google.api.gax.retrying.RetryAlgorithm; +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.retrying.RetryingExecutor; +import com.google.api.gax.retrying.RetryingFuture; +import com.google.api.gax.retrying.TimedRetryAlgorithm; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +/** + * Utility class for retrying operations. For more details about the parameters, see {@link + * RetrySettings}. In case if retrying is unsuccessful, {@link RetryHelperException} will be thrown. + */ +@BetaApi +public class RetryHelper { + public static V runWithRetries( + Callable callable, + RetrySettings retrySettings, + ResultRetryAlgorithm resultRetryAlgorithm, + ApiClock clock) + throws RetryHelperException { + try { + // Suppressing should be ok as a workaraund. Current and only ResultRetryAlgorithm + // implementation does not use response at all, so ignoring its type is ok. + @SuppressWarnings("unchecked") + ResultRetryAlgorithm algorithm = (ResultRetryAlgorithm) resultRetryAlgorithm; + return run(callable, new ExponentialRetryAlgorithm(retrySettings, clock), algorithm); + } catch (Exception e) { + // TODO: remove RetryHelperException, throw InterruptedException or + // ExecutionException#getCause() explicitly + throw new RetryHelperException(e.getCause()); + } + } + + public static V poll( + Callable callable, + RetrySettings pollSettings, + ResultRetryAlgorithm resultPollAlgorithm, + ApiClock clock) + throws ExecutionException, InterruptedException { + return run(callable, new ExponentialPollAlgorithm(pollSettings, clock), resultPollAlgorithm); + } + + private static V run( + Callable callable, + TimedRetryAlgorithm timedAlgorithm, + ResultRetryAlgorithm resultAlgorithm) + throws ExecutionException, InterruptedException { + RetryAlgorithm retryAlgorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + RetryingExecutor executor = new DirectRetryingExecutor<>(retryAlgorithm); + + RetryingFuture retryingFuture = executor.createFuture(callable); + executor.submit(retryingFuture); + return retryingFuture.get(); + } + + public static class RetryHelperException extends RuntimeException { + + private static final long serialVersionUID = -8519852520090965314L; + + RetryHelperException(Throwable cause) { + super(cause); + } + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/RetryOption.java b/google-cloud-core/src/main/java/com/google/cloud/RetryOption.java new file mode 100644 index 0000000000..a1069b48a2 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/RetryOption.java @@ -0,0 +1,150 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.core.BetaApi; +import com.google.api.gax.retrying.RetrySettings; +import java.io.Serializable; +import org.threeten.bp.Duration; + +/** + * This class represents an options wrapper around the {@link RetrySettings} class and is an + * alternative way of initializing it. The retry options are usually provided in a form of varargs + * for methods that wait for changes in the status of a resource, do poll operations or retry on + * failures. + */ +@BetaApi +public class RetryOption implements Serializable { + + private static final long serialVersionUID = 3622837212525370224L; + + private enum OptionType { + TOTAL_TIMEOUT, + INITIAL_RETRY_DELAY, + RETRY_DELAY_MULTIPLIER, + MAX_RETRY_DELAY, + MAX_ATTEMPTS, + JITTERED + } + + private final OptionType type; + private final Object value; + + private RetryOption(OptionType type, Object value) { + this.type = checkNotNull(type); + this.value = checkNotNull(value); + } + + /** See {@link RetrySettings#getTotalTimeout()}. */ + public static RetryOption totalTimeout(Duration totalTimeout) { + return new RetryOption(OptionType.TOTAL_TIMEOUT, totalTimeout); + } + + /** See {@link RetrySettings#getInitialRetryDelay()}. */ + public static RetryOption initialRetryDelay(Duration initialRetryDelay) { + return new RetryOption(OptionType.INITIAL_RETRY_DELAY, initialRetryDelay); + } + + /** See {@link RetrySettings#getRetryDelayMultiplier()}. */ + public static RetryOption retryDelayMultiplier(double retryDelayMultiplier) { + return new RetryOption(OptionType.RETRY_DELAY_MULTIPLIER, retryDelayMultiplier); + } + + /** See {@link RetrySettings#getMaxRetryDelay()}. */ + public static RetryOption maxRetryDelay(Duration maxRetryDelay) { + return new RetryOption(OptionType.MAX_RETRY_DELAY, maxRetryDelay); + } + + /** See {@link RetrySettings#getMaxAttempts()}. */ + public static RetryOption maxAttempts(int maxAttempts) { + return new RetryOption(OptionType.MAX_ATTEMPTS, maxAttempts); + } + + /** See {@link RetrySettings#isJittered()} ()}. */ + public static RetryOption jittered(boolean jittered) { + return new RetryOption(OptionType.JITTERED, jittered); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + RetryOption that = (RetryOption) o; + + if (type != that.type) { + return false; + } + return value.equals(that.value); + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + value.hashCode(); + return result; + } + + /** + * Creates a new {@code RetrySettings} instance, merging provided settings and multiple {@code + * RetryOptions}, each of which represents a single property in {@code RetrySettings}. It is an + * alternative way of initializing {@link RetrySettings} instances. + * + * @param settings retry settings + * @param options zero or more Retry + * @return new {@code RetrySettings} instance, which is a result of merging {@code options} into + * {@code settings}, i.e. each element in {@code options}, if present, overrides corresponding + * property in {@code settings} + */ + public static RetrySettings mergeToSettings(RetrySettings settings, RetryOption... options) { + if (options.length <= 0) { + return settings; + } + RetrySettings.Builder builder = settings.toBuilder(); + for (RetryOption option : options) { + switch (option.type) { + case TOTAL_TIMEOUT: + builder.setTotalTimeout((Duration) option.value); + break; + case INITIAL_RETRY_DELAY: + builder.setInitialRetryDelay((Duration) option.value); + break; + case RETRY_DELAY_MULTIPLIER: + builder.setRetryDelayMultiplier((Double) option.value); + break; + case MAX_RETRY_DELAY: + builder.setMaxRetryDelay((Duration) option.value); + break; + case MAX_ATTEMPTS: + builder.setMaxAttempts((Integer) option.value); + break; + case JITTERED: + builder.setJittered((Boolean) option.value); + break; + default: + throw new IllegalArgumentException("Unknown option type: " + option.type); + } + } + return builder.build(); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/Role.java b/google-cloud-core/src/main/java/com/google/cloud/Role.java new file mode 100644 index 0000000000..e75dd5f8f2 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/Role.java @@ -0,0 +1,116 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.Serializable; +import java.util.Objects; + +/** + * A role in a IAM {@link Policy}. Project owners can grant access to team members to access + * project's resources and APIs by granting IAM roles to team members. + * + * @see Viewing the Grantable + * Roles on Resources + * @see Understanding Roles + */ +public final class Role implements Serializable { + + private static final long serialVersionUID = -7779252712160972508L; + private static final String ROLE_PREFIX = "roles/"; + + private final String value; + + private Role(String value) { + this.value = value; + } + + /** + * Returns the string identifier for this role. For example, {@code "roles/viewer"}, {@code + * "roles/editor"}, or {@code "roles/owner"}. + */ + public String getValue() { + return value; + } + + /** + * Returns the viewer role. Encapsulates the permission for read-only actions that preserve state + * of a resource. + * + * @see Understanding Roles + */ + public static Role viewer() { + return of("viewer"); + } + + /** + * Returns the editor role. Encapsulates all viewer's permissions and permissions for actions that + * modify the state of a resource. + * + * @see Understanding Roles + */ + public static Role editor() { + return of("editor"); + } + + /** + * Returns the owner role. Encapsulates all editor's permissions and permissions to manage access + * control for a resource or manage the billing options for a project. + * + * @see Understanding Roles + */ + public static Role owner() { + return of("owner"); + } + + /** + * Returns a new role given its string value. + * + *

If the value contains no slash character ({@code '/'}), the prefix {@code "roles/""} is + * prepended. This slightly simplifies usage for predefined roles. For custom roles, call this + * method with the fully-qualified name, eg {@code "projects/XXX/roles/YYY"}. + * + * @param value the string value for the role + * @see Viewing the Grantable + * Roles on Resources + */ + public static Role of(String value) { + checkNotNull(value); + if (!value.contains("/")) { + value = ROLE_PREFIX + value; + } + return new Role(value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Role && Objects.equals(value, ((Role) obj).getValue()); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/Service.java b/google-cloud-core/src/main/java/com/google/cloud/Service.java new file mode 100644 index 0000000000..bfacabdf7f --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/Service.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +/** + * Interface for service objects. + * + * @param the {@code ServiceOptions} subclass corresponding to the service + */ +public interface Service> { + + OptionsT getOptions(); +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/ServiceDefaults.java b/google-cloud-core/src/main/java/com/google/cloud/ServiceDefaults.java new file mode 100644 index 0000000000..7151740d99 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/ServiceDefaults.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.cloud.spi.ServiceRpcFactory; +import java.io.Serializable; + +public interface ServiceDefaults< + ServiceT extends Service, OptionsT extends ServiceOptions> + extends Serializable { + ServiceFactory getDefaultServiceFactory(); + + ServiceRpcFactory getDefaultRpcFactory(); + + TransportOptions getDefaultTransportOptions(); +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/ServiceFactory.java b/google-cloud-core/src/main/java/com/google/cloud/ServiceFactory.java new file mode 100644 index 0000000000..3b8fefe52d --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/ServiceFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +/** + * A base interface for all service factories. + * + *

Implementation must provide a public no-arg constructor. Loading of a factory implementation + * is done via {@link java.util.ServiceLoader}. + * + * @param the service subclass + * @param the {@code ServiceOptions} subclass corresponding to the service + */ +@SuppressWarnings("rawtypes") +public interface ServiceFactory { + + ServiceT create(ServiceOptionsT serviceOptions); +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java b/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java new file mode 100644 index 0000000000..56f2d19595 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java @@ -0,0 +1,737 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +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.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.Charsets; +import com.google.api.core.ApiClock; +import com.google.api.core.BetaApi; +import com.google.api.core.CurrentMillisClock; +import com.google.api.core.InternalApi; +import com.google.api.gax.core.GaxProperties; +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.rpc.FixedHeaderProvider; +import com.google.api.gax.rpc.HeaderProvider; +import com.google.api.gax.rpc.NoHeaderProvider; +import com.google.auth.Credentials; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.spi.ServiceRpcFactory; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.io.Files; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.threeten.bp.Duration; + +/** + * Abstract class representing service options. + * + * @param the service subclass + * @param the {@code ServiceOptions} subclass corresponding to the service + */ +public abstract class ServiceOptions< + ServiceT extends Service, OptionsT extends ServiceOptions> + implements Serializable { + + public static final String CREDENTIAL_ENV_NAME = "GOOGLE_APPLICATION_CREDENTIALS"; + + private static final String DEFAULT_HOST = "https://www.googleapis.com"; + private static final String LEGACY_PROJECT_ENV_NAME = "GCLOUD_PROJECT"; + private static final String PROJECT_ENV_NAME = "GOOGLE_CLOUD_PROJECT"; + + private static final RetrySettings DEFAULT_RETRY_SETTINGS = + getDefaultRetrySettingsBuilder().build(); + private static final RetrySettings NO_RETRY_SETTINGS = + getDefaultRetrySettingsBuilder().setMaxAttempts(1).build(); + + private static final long serialVersionUID = 9198896031667942014L; + protected final String clientLibToken; + + private final String projectId; + private final String host; + private final RetrySettings retrySettings; + private final String serviceRpcFactoryClassName; + private final String serviceFactoryClassName; + private final ApiClock clock; + protected Credentials credentials; + private final TransportOptions transportOptions; + private final HeaderProvider headerProvider; + + private transient ServiceRpcFactory serviceRpcFactory; + private transient ServiceFactory serviceFactory; + private transient ServiceT service; + private transient ServiceRpc rpc; + + /** + * Builder for {@code ServiceOptions}. + * + * @param the service subclass + * @param the {@code ServiceOptions} subclass corresponding to the service + * @param the {@code ServiceOptions} builder + */ + public abstract static class Builder< + ServiceT extends Service, + OptionsT extends ServiceOptions, + B extends Builder> { + + private final ImmutableSet allowedClientLibTokens = + ImmutableSet.of(ServiceOptions.getGoogApiClientLibName()); + private String projectId; + private String host; + protected Credentials credentials; + private RetrySettings retrySettings; + private ServiceFactory serviceFactory; + private ServiceRpcFactory serviceRpcFactory; + private ApiClock clock; + private TransportOptions transportOptions; + private HeaderProvider headerProvider; + private String clientLibToken = ServiceOptions.getGoogApiClientLibName(); + + @InternalApi("This class should only be extended within google-cloud-java") + protected Builder() {} + + @InternalApi("This class should only be extended within google-cloud-java") + protected Builder(ServiceOptions options) { + projectId = options.projectId; + host = options.host; + credentials = options.credentials; + retrySettings = options.retrySettings; + serviceFactory = options.serviceFactory; + serviceRpcFactory = options.serviceRpcFactory; + clock = options.clock; + transportOptions = options.transportOptions; + clientLibToken = options.clientLibToken; + } + + protected abstract ServiceOptions build(); + + @SuppressWarnings("unchecked") + protected B self() { + return (B) this; + } + + /** Sets the service factory. */ + public B setServiceFactory(ServiceFactory serviceFactory) { + this.serviceFactory = serviceFactory; + return self(); + } + + /** + * Sets the service's clock. The clock is mainly used for testing purpose. {@link ApiClock} will + * be replaced by Java8's {@code java.time.Clock}. + * + * @param clock the clock to set + * @return the builder + */ + public B setClock(ApiClock clock) { + this.clock = clock; + return self(); + } + + /** + * Sets the project ID. If no project ID is set, {@link #getDefaultProjectId()} will be used to + * attempt getting the project ID from the environment. + * + * @return the builder + */ + public B setProjectId(String projectId) { + this.projectId = projectId; + return self(); + } + + /** + * Sets service host. + * + * @return the builder + */ + public B setHost(String host) { + this.host = host; + return self(); + } + + /** + * Sets the service authentication credentials. If no credentials are set, {@link + * GoogleCredentials#getApplicationDefault()} will be used to attempt getting credentials from + * the environment. Use {@link NoCredentials#getInstance()} to skip authentication, this is + * typically useful when using local service emulators. + * + * @param credentials authentication credentials, should not be {@code null} + * @return the builder + * @throws NullPointerException if {@code credentials} is {@code null}. To disable + * authentication use {@link NoCredentials#getInstance()} + */ + public B setCredentials(Credentials credentials) { + this.credentials = checkNotNull(credentials); + // set project id if available + if (this.projectId == null && credentials instanceof ServiceAccountCredentials) { + this.projectId = ((ServiceAccountCredentials) credentials).getProjectId(); + } + return self(); + } + + /** + * Sets configuration parameters for request retries. + * + * @return the builder + */ + public B setRetrySettings(RetrySettings retrySettings) { + this.retrySettings = retrySettings; + return self(); + } + + /** + * Sets the factory for rpc services. + * + * @return the builder + */ + public B setServiceRpcFactory(ServiceRpcFactory serviceRpcFactory) { + this.serviceRpcFactory = serviceRpcFactory; + return self(); + } + + /** + * Sets the transport options. + * + * @return the builder + */ + public B setTransportOptions(TransportOptions transportOptions) { + this.transportOptions = transportOptions; + return self(); + } + + /** + * Sets the static header provider. The header provider will be called during client + * construction only once. The headers returned by the provider will be cached and supplied as + * is for each request issued by the constructed client. Some reserved headers can be overridden + * (e.g. Content-Type) or merged with the default value (e.g. User-Agent) by the underlying + * transport layer. + * + * @param headerProvider the header provider + * @return the builder + */ + @BetaApi + public B setHeaderProvider(HeaderProvider headerProvider) { + this.headerProvider = headerProvider; + return self(); + } + + @InternalApi + public B setClientLibToken(String clientLibToken) { + Preconditions.checkArgument( + getAllowedClientLibTokens().contains(clientLibToken), "Illegal client lib token"); + this.clientLibToken = clientLibToken; + return self(); + } + + protected Set getAllowedClientLibTokens() { + return allowedClientLibTokens; + } + } + + @InternalApi("This class should only be extended within google-cloud-java") + protected ServiceOptions( + Class> serviceFactoryClass, + Class> rpcFactoryClass, + Builder builder, + ServiceDefaults serviceDefaults) { + projectId = builder.projectId != null ? builder.projectId : getDefaultProject(); + if (projectIdRequired()) { + checkArgument( + projectId != null, + "A project ID is required for this service but could not be determined from the builder " + + "or the environment. Please set a project ID using the builder."); + } + host = firstNonNull(builder.host, getDefaultHost()); + credentials = builder.credentials != null ? builder.credentials : defaultCredentials(); + retrySettings = firstNonNull(builder.retrySettings, getDefaultRetrySettings()); + serviceFactory = + firstNonNull( + builder.serviceFactory, + getFromServiceLoader(serviceFactoryClass, serviceDefaults.getDefaultServiceFactory())); + serviceFactoryClassName = serviceFactory.getClass().getName(); + serviceRpcFactory = + firstNonNull( + builder.serviceRpcFactory, + getFromServiceLoader(rpcFactoryClass, serviceDefaults.getDefaultRpcFactory())); + serviceRpcFactoryClassName = serviceRpcFactory.getClass().getName(); + clock = firstNonNull(builder.clock, CurrentMillisClock.getDefaultClock()); + transportOptions = + firstNonNull(builder.transportOptions, serviceDefaults.getDefaultTransportOptions()); + headerProvider = firstNonNull(builder.headerProvider, new NoHeaderProvider()); + clientLibToken = builder.clientLibToken; + } + + /** + * Returns whether a service requires a project ID. This method may be overridden in + * service-specific Options objects. + * + * @return true if a project ID is required to use the service, false if not + */ + protected boolean projectIdRequired() { + return true; + } + + private static GoogleCredentials defaultCredentials() { + try { + return GoogleCredentials.getApplicationDefault(); + } catch (Exception ex) { + return null; + } + } + + protected String getDefaultHost() { + return DEFAULT_HOST; + } + + protected String getDefaultProject() { + return getDefaultProjectId(); + } + + /** + * Returns the default project ID, or {@code null} if no default project ID could be found. This + * method returns the first available project ID among the following sources: + * + *

    + *
  1. The project ID specified by the GOOGLE_CLOUD_PROJECT environment variable + *
  2. The App Engine project ID + *
  3. The project ID specified in the JSON credentials file pointed by the {@code + * GOOGLE_APPLICATION_CREDENTIALS} environment variable + *
  4. The Google Cloud SDK project ID + *
  5. The Compute Engine project ID + *
+ */ + public static String getDefaultProjectId() { + String projectId = System.getProperty(PROJECT_ENV_NAME, System.getenv(PROJECT_ENV_NAME)); + if (projectId == null) { + projectId = + System.getProperty(LEGACY_PROJECT_ENV_NAME, System.getenv(LEGACY_PROJECT_ENV_NAME)); + } + if (projectId == null) { + projectId = getAppEngineProjectId(); + } + if (projectId == null) { + projectId = getServiceAccountProjectId(); + } + return projectId != null ? projectId : getGoogleCloudProjectId(); + } + + public static String getAppEngineAppId() { + return System.getProperty("com.google.appengine.application.id"); + } + + private static String getActiveGoogleCloudConfig(File configDir) { + String activeGoogleCloudConfig = null; + try { + activeGoogleCloudConfig = + Files.asCharSource(new File(configDir, "active_config"), Charset.defaultCharset()) + .readFirstLine(); + } catch (IOException ex) { + // ignore + } + // if reading active_config failed or the file is empty we try default + return firstNonNull(activeGoogleCloudConfig, "default"); + } + + protected static String getGoogleCloudProjectId() { + 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"); + } + String activeConfig = getActiveGoogleCloudConfig(configDir); + FileReader fileReader = null; + try { + fileReader = new FileReader(new File(configDir, "configurations/config_" + activeConfig)); + } catch (FileNotFoundException newConfigFileNotFoundEx) { + try { + fileReader = new FileReader(new File(configDir, "properties")); + } catch (FileNotFoundException oldConfigFileNotFoundEx) { + // ignore + } + } + if (fileReader != null) { + try (BufferedReader reader = new BufferedReader(fileReader)) { + 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 project id from metadata config + return MetadataConfig.getProjectId(); + } + + private static boolean isWindows() { + return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"); + } + + protected static String getAppEngineProjectId() { + String projectId = null; + if (PlatformInformation.isOnGAEStandard7()) { + projectId = getAppEngineProjectIdFromAppId(); + } else { + // for GAE flex and standard Java 8 environment + projectId = System.getenv("GOOGLE_CLOUD_PROJECT"); + if (projectId == null) { + projectId = System.getenv("GCLOUD_PROJECT"); + } + if (projectId == null) { + projectId = getAppEngineProjectIdFromAppId(); + } + if (projectId == null) { + try { + projectId = getAppEngineProjectIdFromMetadataServer(); + } catch (IOException ignore) { + projectId = null; + } + } + } + return projectId; + } + + protected static String getAppEngineProjectIdFromAppId() { + String projectId = getAppEngineAppId(); + if (projectId != null && projectId.contains(":")) { + int colonIndex = projectId.indexOf(":"); + projectId = projectId.substring(colonIndex + 1); + } + return projectId; + } + + private static String getAppEngineProjectIdFromMetadataServer() throws IOException { + String metadata = "http://metadata.google.internal"; + String projectIdURL = "/computeMetadata/v1/project/project-id"; + GenericUrl url = new GenericUrl(metadata + projectIdURL); + + HttpTransport netHttpTransport = new NetHttpTransport(); + HttpRequestFactory requestFactory = netHttpTransport.createRequestFactory(); + HttpRequest request = + requestFactory + .buildGetRequest(url) + .setConnectTimeout(500) + .setReadTimeout(500) + .setHeaders(new HttpHeaders().set("Metadata-Flavor", "Google")); + HttpResponse response = request.execute(); + return headerContainsMetadataFlavor(response) ? response.parseAsString() : null; + } + + @InternalApi("Visible for testing") + static boolean headerContainsMetadataFlavor(HttpResponse response) { + String metadataFlavorValue = response.getHeaders().getFirstHeaderStringValue("Metadata-Flavor"); + return "Google".equals(metadataFlavorValue); + } + + protected static String getServiceAccountProjectId() { + return getServiceAccountProjectId(System.getenv(CREDENTIAL_ENV_NAME)); + } + + @InternalApi("Visible for testing") + static String getServiceAccountProjectId(String credentialsPath) { + String project = null; + if (credentialsPath != null) { + try (InputStream credentialsStream = new FileInputStream(credentialsPath)) { + JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); + JsonObjectParser parser = new JsonObjectParser(jsonFactory); + GenericJson fileContents = + parser.parseAndClose(credentialsStream, Charsets.UTF_8, GenericJson.class); + project = (String) fileContents.get("project_id"); + } catch (IOException e) { + // ignore + } + } + return project; + } + + /** + * Returns a Service object for the current service. For instance, when using Google Cloud + * Storage, it returns a Storage object. + */ + @SuppressWarnings("unchecked") + public ServiceT getService() { + if (shouldRefreshService(service)) { + service = serviceFactory.create((OptionsT) this); + } + return service; + } + + /** + * @param cachedService The currently cached service object + * @return true if the currently cached service object should be refreshed. + */ + protected boolean shouldRefreshService(ServiceT cachedService) { + return cachedService == null; + } + + /** + * Returns a Service RPC object for the current service. For instance, when using Google Cloud + * Storage, it returns a StorageRpc object. + */ + @SuppressWarnings("unchecked") + public ServiceRpc getRpc() { + if (shouldRefreshRpc(rpc)) { + rpc = serviceRpcFactory.create((OptionsT) this); + } + return rpc; + } + + /** + * @param cachedRpc The currently cached service object + * @return true if the currently cached service object should be refreshed. + */ + protected boolean shouldRefreshRpc(ServiceRpc cachedRpc) { + return cachedRpc == null; + } + + /** + * Returns the project ID. Return value can be null (for services that don't require a project + * ID). + */ + public String getProjectId() { + return projectId; + } + + /** Returns the service host. */ + public String getHost() { + return host; + } + + /** Returns the authentication credentials. */ + public Credentials getCredentials() { + return credentials; + } + + /** Returns the authentication credentials. If required, credentials are scoped. */ + public Credentials getScopedCredentials() { + Credentials credentialsToReturn = credentials; + if (credentials instanceof GoogleCredentials + && ((GoogleCredentials) credentials).createScopedRequired()) { + credentialsToReturn = ((GoogleCredentials) credentials).createScoped(getScopes()); + } + return credentialsToReturn; + } + + /** Returns configuration parameters for request retries. */ + public RetrySettings getRetrySettings() { + return retrySettings; + } + + /** + * Returns the service's clock. Default time source uses {@link System#currentTimeMillis()} to get + * current time. + */ + public ApiClock getClock() { + return clock; + } + + /** Returns the transport-specific options for this service. */ + public TransportOptions getTransportOptions() { + return transportOptions; + } + + /** + * Returns the application's name as a string in the format {@code gcloud-java/[version]}, + * optionally prepended with externally supplied User-Agent header value (via setting custom + * header provider). + */ + public String getApplicationName() { + String libraryVersion = getLibraryVersion(); + + // We have to do the following since underlying layers often do not appreciate User-Agent + // provided as a normal header and override it or treat setting "application name" as the only + // way to append something to User-Agent header. + StringBuilder sb = new StringBuilder(); + String customUserAgentValue = getUserAgent(); + if (customUserAgentValue != null) { + sb.append(customUserAgentValue).append(' '); + } + if (libraryVersion == null) { + sb.append(getLibraryName()); + } else { + sb.append(getLibraryName()).append('/').append(libraryVersion); + } + + return sb.toString(); + } + + /** Returns the library's name, {@code gcloud-java}, as a string. */ + public static String getLibraryName() { + return "gcloud-java"; + } + + /** Returns the library's name used by x-goog-api-client header as a string. */ + public static String getGoogApiClientLibName() { + return "gccl"; + } + + /** Returns the library's version as a string. */ + public String getLibraryVersion() { + return GaxProperties.getLibraryVersion(this.getClass()); + } + + @InternalApi + public final HeaderProvider getMergedHeaderProvider(HeaderProvider internalHeaderProvider) { + Map mergedHeaders = + ImmutableMap.builder() + .putAll(internalHeaderProvider.getHeaders()) + .putAll(headerProvider.getHeaders()) + .build(); + return FixedHeaderProvider.create(mergedHeaders); + } + + @InternalApi + public final String getUserAgent() { + if (headerProvider != null) { + for (Map.Entry entry : headerProvider.getHeaders().entrySet()) { + if ("user-agent".equals(entry.getKey().toLowerCase())) { + return entry.getValue(); + } + } + } + return null; + } + + protected int baseHashCode() { + return Objects.hash( + projectId, + host, + credentials, + retrySettings, + serviceFactoryClassName, + serviceRpcFactoryClassName, + clock); + } + + protected boolean baseEquals(ServiceOptions other) { + return Objects.equals(projectId, other.projectId) + && Objects.equals(host, other.host) + && Objects.equals(credentials, other.credentials) + && Objects.equals(retrySettings, other.retrySettings) + && Objects.equals(serviceFactoryClassName, other.serviceFactoryClassName) + && Objects.equals(serviceRpcFactoryClassName, other.serviceRpcFactoryClassName) + && Objects.equals(clock, clock); + } + + private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { + input.defaultReadObject(); + serviceFactory = newInstance(serviceFactoryClassName); + serviceRpcFactory = newInstance(serviceRpcFactoryClassName); + } + + @SuppressWarnings("unchecked") + @InternalApi + public static T newInstance(String className) throws IOException, ClassNotFoundException { + try { + return (T) Class.forName(className).newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new IOException(e); + } + } + + public static RetrySettings getDefaultRetrySettings() { + return DEFAULT_RETRY_SETTINGS; + } + + public static RetrySettings getNoRetrySettings() { + return NO_RETRY_SETTINGS; + } + + private static RetrySettings.Builder getDefaultRetrySettingsBuilder() { + return RetrySettings.newBuilder() + .setMaxAttempts(6) + .setInitialRetryDelay(Duration.ofMillis(1000L)) + .setMaxRetryDelay(Duration.ofMillis(32_000L)) + .setRetryDelayMultiplier(2.0) + .setTotalTimeout(Duration.ofMillis(50_000L)) + .setInitialRpcTimeout(Duration.ofMillis(50_000L)) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeout(Duration.ofMillis(50_000L)); + } + + protected abstract Set getScopes(); + + public abstract > B toBuilder(); + + /** + * Some services may have different backoff requirements listed in their SLAs. Be sure to override + * this method in options subclasses when the service's backoff requirement differs from the + * default parameters listed in {@link RetrySettings}. + */ + protected RetrySettings defaultRetrySettings() { + return getDefaultRetrySettings(); + } + + @InternalApi + public static T getFromServiceLoader(Class clazz, T defaultInstance) { + return Iterables.getFirst(ServiceLoader.load(clazz), defaultInstance); + } + + public String getClientLibToken() { + return clientLibToken; + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/ServiceRpc.java b/google-cloud-core/src/main/java/com/google/cloud/ServiceRpc.java new file mode 100644 index 0000000000..ce4bd18a6a --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/ServiceRpc.java @@ -0,0 +1,19 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +public interface ServiceRpc {} diff --git a/google-cloud-core/src/main/java/com/google/cloud/StringEnumType.java b/google-cloud-core/src/main/java/com/google/cloud/StringEnumType.java new file mode 100644 index 0000000000..647e53f8e3 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/StringEnumType.java @@ -0,0 +1,90 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud; + +import com.google.api.core.ApiFunction; +import com.google.api.core.InternalApi; +import com.google.common.base.Preconditions; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * This represents a concept having a known set of acceptable String values, which can expand later + * due to new API features. + */ +@InternalApi +public class StringEnumType { + + private final Class clazz; + private final ApiFunction constructor; + private final Map knownValues = new LinkedHashMap<>(); + + public StringEnumType(Class clazz, ApiFunction constructor) { + this.clazz = Preconditions.checkNotNull(clazz); + this.constructor = Preconditions.checkNotNull(constructor); + } + + /** Create a new constant and register it in the known values. */ + public EnumT createAndRegister(String constant) { + EnumT instance = constructor.apply(constant); + knownValues.put(constant, instance); + return instance; + } + + /** + * Get the enum object for the given String constant, and throw an exception if the constant is + * not recognized. + */ + public EnumT valueOfStrict(String constant) { + EnumT value = knownValues.get(constant); + if (value != null) { + return value; + } else { + throw new IllegalArgumentException( + "Constant \"" + constant + "\" not found for enum \"" + clazz.getName() + "\""); + } + } + + /** Get the enum object for the given String constant, and allow unrecognized values. */ + public EnumT valueOf(String constant) { + if (constant == null || constant.isEmpty()) { + throw new IllegalArgumentException("Empty enum constants not allowed."); + } + EnumT value = knownValues.get(constant); + if (value != null) { + return value; + } else { + return constructor.apply(constant); + } + } + + /** Return the known values of this enum type. */ + public EnumT[] values() { + Collection valueCollection = knownValues.values(); + + @SuppressWarnings("unchecked") + final EnumT[] valueArray = (EnumT[]) Array.newInstance(clazz, valueCollection.size()); + int i = 0; + for (EnumT enumV : valueCollection) { + valueArray[i] = enumV; + i++; + } + + return valueArray; + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/StringEnumValue.java b/google-cloud-core/src/main/java/com/google/cloud/StringEnumValue.java new file mode 100644 index 0000000000..2f1ae94218 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/StringEnumValue.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud; + +import com.google.api.core.InternalApi; +import com.google.common.base.Preconditions; +import java.io.Serializable; + +/** + * This represents a specific instance of a concept having a known set of acceptable String values, + * which can expand later due to new API features. Standard Java enums can't be used in such a + * context. + */ +public abstract class StringEnumValue implements Serializable { + private static final long serialVersionUID = 1501809419544297884L; + private final String constant; + + /** Don't create subclasses outside of google-cloud-java. */ + @InternalApi("This class should only be extended within google-cloud-java") + protected StringEnumValue(String constant) { + this.constant = Preconditions.checkNotNull(constant); + } + + public String name() { + return constant; + } + + @Override + public String toString() { + return constant; + } + + @Override + public boolean equals(Object that) { + if (that == null) { + return false; + } + if (this == that) { + return true; + } + if (!(getClass().equals(that.getClass()))) { + return false; + } + StringEnumValue thatEnumValue = (StringEnumValue) that; + return this.constant.equals(thatEnumValue.constant); + } + + @Override + public int hashCode() { + return constant.hashCode(); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/Timestamp.java b/google-cloud-core/src/main/java/com/google/cloud/Timestamp.java new file mode 100644 index 0000000000..618ef9c721 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/Timestamp.java @@ -0,0 +1,221 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.protobuf.util.Timestamps; +import java.io.Serializable; +import java.util.Date; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.threeten.bp.Instant; +import org.threeten.bp.LocalDateTime; +import org.threeten.bp.ZoneOffset; +import org.threeten.bp.format.DateTimeFormatter; +import org.threeten.bp.format.DateTimeFormatterBuilder; +import org.threeten.bp.temporal.TemporalAccessor; + +/** + * Represents a timestamp with nanosecond precision. Timestamps cover the range [0001-01-01, + * 9999-12-31]. + * + *

{@code Timestamp} instances are immutable. + */ +public final class Timestamp implements Comparable, Serializable { + + private static final long serialVersionUID = 5152143600571559844L; + + /** The smallest legal timestamp ("0001-01-01T00:00:00Z"). */ + public static final Timestamp MIN_VALUE = new Timestamp(-62135596800L, 0); + + /** The largest legal timestamp ("9999-12-31T23:59:59Z"). */ + public static final Timestamp MAX_VALUE = + new Timestamp(253402300799L, (int) TimeUnit.SECONDS.toNanos(1) - 1); + + private static final DateTimeFormatter format = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + + private static final DateTimeFormatter timestampParser = + new DateTimeFormatterBuilder() + .appendOptional(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .optionalStart() + .appendOffsetId() + .optionalEnd() + .toFormatter() + .withZone(ZoneOffset.UTC); + + private final long seconds; + private final int nanos; + + private Timestamp(long seconds, int nanos) { + this.seconds = seconds; + this.nanos = nanos; + } + + /** + * Creates an instance representing the value of {@code seconds} and {@code nanos} since January + * 1, 1970, 00:00:00 UTC. + * + * @param seconds seconds since January 1, 1970, 00:00:00 UTC. A negative value is the number of + * seconds before January 1, 1970, 00:00:00 UTC. + * @param nanos the fractional seconds component, in the range 0..999999999. + * @throws IllegalArgumentException if the timestamp is outside the representable range + */ + public static Timestamp ofTimeSecondsAndNanos(long seconds, int nanos) { + checkArgument( + Timestamps.isValid(seconds, nanos), "timestamp out of range: %s, %s", seconds, nanos); + return new Timestamp(seconds, nanos); + } + + /** + * Creates an instance representing the value of {@code microseconds}. + * + * @throws IllegalArgumentException if the timestamp is outside the representable range + */ + public static Timestamp ofTimeMicroseconds(long microseconds) { + long seconds = microseconds / 1_000_000; + int nanos = (int) (microseconds % 1_000_000 * 1000); + if (nanos < 0) { + seconds--; + nanos += 1_000_000_000; + } + checkArgument( + Timestamps.isValid(seconds, nanos), "timestamp out of range: %s, %s", seconds, nanos); + return new Timestamp(seconds, nanos); + } + + /** + * Creates an instance representing the value of {@code Date}. + * + * @throws IllegalArgumentException if the timestamp is outside the representable range + */ + public static Timestamp of(Date date) { + return ofTimeMicroseconds(TimeUnit.MILLISECONDS.toMicros(date.getTime())); + } + + /** Creates an instance with current time. */ + public static Timestamp now() { + java.sql.Timestamp date = new java.sql.Timestamp(System.currentTimeMillis()); + return of(date); + } + + /** + * Creates an instance representing the value of {@code timestamp}. + * + * @throws IllegalArgumentException if the timestamp is outside the representable range + */ + public static Timestamp of(java.sql.Timestamp timestamp) { + return ofTimeSecondsAndNanos(timestamp.getTime() / 1000, timestamp.getNanos()); + } + + /** + * Returns the number of seconds since January 1, 1970, 00:00:00 UTC. A negative value is the + * number of seconds before January 1, 1970, 00:00:00 UTC. + */ + public long getSeconds() { + return seconds; + } + + /** Returns the fractional seconds component, in nanoseconds. */ + public int getNanos() { + return nanos; + } + + /** Returns a JDBC timestamp initialized to the same point in time as {@code this}. */ + public java.sql.Timestamp toSqlTimestamp() { + java.sql.Timestamp ts = new java.sql.Timestamp(seconds * 1000); + ts.setNanos(nanos); + return ts; + } + + /** + * Returns a new {@code java.util.Date} corresponding to this {@code timestamp}. Any + * sub-millisecond precision will be stripped. + * + * @return An approximate {@code java.util.Date} representation of this {@code timestamp}. + */ + public Date toDate() { + long secondsInMilliseconds = TimeUnit.SECONDS.toMillis(this.seconds); + long nanosInMilliseconds = TimeUnit.NANOSECONDS.toMillis(this.nanos); + return new Date(secondsInMilliseconds + nanosInMilliseconds); + } + + @Override + public int compareTo(Timestamp other) { + int r = Long.compare(seconds, other.seconds); + if (r == 0) { + r = Integer.compare(nanos, other.nanos); + } + return r; + } + + /** Creates an instance of Timestamp from {@code com.google.protobuf.Timestamp}. */ + public static Timestamp fromProto(com.google.protobuf.Timestamp proto) { + return new Timestamp(proto.getSeconds(), proto.getNanos()); + } + + /** + * Returns a {@code com.google.protobuf.Timestamp} initialized to the same point in time as {@code + * this}. + */ + public com.google.protobuf.Timestamp toProto() { + return com.google.protobuf.Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build(); + } + + /** + * Creates a Timestamp instance from the given string. String is in the RFC 3339 format without + * the timezone offset (always ends in "Z"). + */ + public static Timestamp parseTimestamp(String timestamp) { + TemporalAccessor temporalAccessor = timestampParser.parse(timestamp); + Instant instant = Instant.from(temporalAccessor); + return ofTimeSecondsAndNanos(instant.getEpochSecond(), instant.getNano()); + } + + private StringBuilder toString(StringBuilder b) { + format.formatTo(LocalDateTime.ofEpochSecond(seconds, 0, ZoneOffset.UTC), b); + if (nanos != 0) { + b.append(String.format(".%09d", nanos)); + } + b.append('Z'); + return b; + } + + @Override + public String toString() { + return toString(new StringBuilder()).toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Timestamp that = (Timestamp) o; + return seconds == that.seconds && nanos == that.nanos; + } + + @Override + public int hashCode() { + return Objects.hash(seconds, nanos); + } + + // TODO(user): Consider adding math operations. +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/TransportOptions.java b/google-cloud-core/src/main/java/com/google/cloud/TransportOptions.java new file mode 100644 index 0000000000..e62fdddefc --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/TransportOptions.java @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud; + +import java.io.Serializable; + +/** An abstraction for transport-specific options, e.g. for http1.1 vs grpc. */ +public interface TransportOptions extends Serializable {} diff --git a/google-cloud-core/src/main/java/com/google/cloud/Tuple.java b/google-cloud-core/src/main/java/com/google/cloud/Tuple.java new file mode 100644 index 0000000000..c9d2b9bc25 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/Tuple.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +public 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; + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/WriteChannel.java b/google-cloud-core/src/main/java/com/google/cloud/WriteChannel.java new file mode 100644 index 0000000000..947909bb66 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/WriteChannel.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import java.io.Closeable; +import java.nio.channels.WritableByteChannel; + +/** + * A channel for writing data to Google Cloud services. + * + *

Implementations of this class may further buffer data internally to reduce remote calls. + * Written data might not be visible until calling {@link #close()}. This interface implements + * {@link Restorable} to allow saving the writer's state to continue writing afterwards. + */ +public interface WriteChannel extends WritableByteChannel, Closeable, Restorable { + + /** + * 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 setChunkSize(int chunkSize); + + /** + * Captures the write channel state so that it can be saved and restored afterwards. The original + * {@code WriteChannel} and the restored one should not both be used. Closing one channel causes + * the other channel to close; subsequent writes will fail. + * + * @return a {@link RestorableState} object that contains the write channel state and can restore + * it afterwards. + */ + @Override + RestorableState capture(); +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/package-info.java b/google-cloud-core/src/main/java/com/google/cloud/package-info.java new file mode 100644 index 0000000000..56d9be78d6 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2016 Google LLC + * + * 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. + */ + +/** Core classes for the {@code google-cloud} library. */ +package com.google.cloud; diff --git a/google-cloud-core/src/main/java/com/google/cloud/spi/ServiceRpcFactory.java b/google-cloud-core/src/main/java/com/google/cloud/spi/ServiceRpcFactory.java new file mode 100644 index 0000000000..bd27986e1d --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/spi/ServiceRpcFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spi; + +import com.google.cloud.ServiceOptions; +import com.google.cloud.ServiceRpc; + +/** + * A base interface for all service RPC factories. Implementation must provide a public no-arg + * constructor. Loading of a factory implementation is done via {@link java.util.ServiceLoader}. + */ +@SuppressWarnings("rawtypes") +public interface ServiceRpcFactory { + + ServiceRpc create(OptionsT options); +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/testing/BaseEmulatorHelper.java b/google-cloud-core/src/main/java/com/google/cloud/testing/BaseEmulatorHelper.java new file mode 100644 index 0000000000..00291f2b4b --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/testing/BaseEmulatorHelper.java @@ -0,0 +1,467 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.testing; + +import com.google.api.core.CurrentMillisClock; +import com.google.api.core.InternalApi; +import com.google.cloud.ExceptionHandler; +import com.google.cloud.RetryHelper; +import com.google.cloud.ServiceOptions; +import com.google.common.io.CharStreams; +import com.google.common.util.concurrent.SettableFuture; +import com.google.common.util.concurrent.UncheckedExecutionException; +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.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.threeten.bp.Duration; + +/** Utility class to start and stop a local service which is used by unit testing. */ +@InternalApi +public abstract class BaseEmulatorHelper { + + private final String emulator; + private final int port; + private final String projectId; + private EmulatorRunner activeRunner; + private BlockingProcessStreamReader blockingProcessReader; + + protected static final String PROJECT_ID_PREFIX = "test-project-"; + protected static final String DEFAULT_HOST = "localhost"; + protected static final int DEFAULT_PORT = 8080; + + @InternalApi("This class should only be extended within google-cloud-java") + protected BaseEmulatorHelper(String emulator, int port, String projectId) { + this.emulator = emulator; + this.port = port > 0 ? port : DEFAULT_PORT; + this.projectId = projectId; + } + + /** + * Returns the emulator runners supported by this emulator. Runners are evaluated in order, the + * first available runner is selected and executed + */ + protected abstract List getEmulatorRunners(); + + /** Returns a logger. */ + protected abstract Logger getLogger(); + + /** + * Starts the local service as a subprocess. Blocks the execution until {@code blockUntilOutput} + * is found on stdout. + */ + protected final void startProcess(String blockUntilOutput) + throws IOException, InterruptedException { + for (EmulatorRunner runner : getEmulatorRunners()) { + // Iterate through all emulator runners until find first available runner. + if (runner.isAvailable()) { + activeRunner = runner; + runner.start(); + break; + } + } + if (activeRunner != null) { + blockingProcessReader = + BlockingProcessStreamReader.start( + emulator, activeRunner.getProcess().getInputStream(), blockUntilOutput, getLogger()); + } else { + // No available runner found. + throw new IOException("No available emulator runner is found."); + } + } + + /** + * Waits for the local service's subprocess to terminate, and stop any possible thread listening + * for its output. + */ + protected final int waitForProcess(Duration timeout) + throws IOException, InterruptedException, TimeoutException { + if (activeRunner != null) { + int exitCode = activeRunner.waitFor(timeout); + activeRunner = null; + return exitCode; + } + if (blockingProcessReader != null) { + blockingProcessReader.join(); + blockingProcessReader = null; + } + return 0; + } + + private static int waitForProcess(final Process process, Duration timeout) + throws InterruptedException, TimeoutException { + if (process == null) { + return 0; + } + + final SettableFuture exitValue = SettableFuture.create(); + + Thread waiter = + new Thread( + new Runnable() { + @Override + public void run() { + try { + exitValue.set(process.waitFor()); + } catch (InterruptedException e) { + exitValue.setException(e); + } + } + }); + waiter.start(); + + try { + return exitValue.get(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + if (e.getCause() instanceof InterruptedException) { + throw (InterruptedException) e.getCause(); + } + throw new UncheckedExecutionException(e); + } finally { + waiter.interrupt(); + } + } + + /** Returns the port to which the local emulator is listening. */ + public int getPort() { + return port; + } + + /** Returns the project ID associated with the local emulator. */ + public String getProjectId() { + return projectId; + } + + /** Returns service options to access the local emulator. */ + public abstract T getOptions(); + + /** Starts the local emulator. */ + public abstract void start() throws IOException, InterruptedException; + + /** Stops the local emulator. */ + public abstract void stop(Duration timeout) + throws IOException, InterruptedException, TimeoutException; + + /** Resets the internal state of the emulator. */ + public abstract void reset() throws IOException; + + protected final String sendPostRequest(String request) throws IOException { + URL url = new URL("http", DEFAULT_HOST, this.port, request); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("POST"); + con.setDoOutput(true); + OutputStream out = con.getOutputStream(); + out.write("".getBytes()); + out.flush(); + + InputStream in = con.getInputStream(); + String response = CharStreams.toString(new InputStreamReader(con.getInputStream())); + in.close(); + return response; + } + + protected static int findAvailablePort(int defaultPort) { + try (ServerSocket tempSocket = new ServerSocket(0)) { + return tempSocket.getLocalPort(); + } catch (IOException e) { + return defaultPort; + } + } + + protected static boolean isWindows() { + return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"); + } + + /** Utility interface to start and run an emulator. */ + protected interface EmulatorRunner { + + /** + * Returns {@code true} if the emulator associated to this runner is available and can be + * started. + */ + boolean isAvailable(); + + /** Starts the emulator associated to this runner. */ + void start() throws IOException; + + /** Wait for the emulator associated to this runner to terminate, returning the exit status. */ + int waitFor(Duration timeout) throws InterruptedException, TimeoutException; + + /** Returns the process associated to the emulator, if any. */ + Process getProcess(); + } + + /** Utility class to start and run an emulator from the Google Cloud SDK. */ + protected static class GcloudEmulatorRunner implements EmulatorRunner { + + private final List commandText; + private final String versionPrefix; + private final Version minVersion; + private Process process; + private static final Logger log = Logger.getLogger(GcloudEmulatorRunner.class.getName()); + + public GcloudEmulatorRunner(List commandText, String versionPrefix, String minVersion) { + this.commandText = commandText; + this.versionPrefix = versionPrefix; + this.minVersion = Version.fromString(minVersion); + } + + @Override + public boolean isAvailable() { + try { + return isGcloudInstalled() && isEmulatorUpToDate() && !commandText.isEmpty(); + } catch (IOException | InterruptedException e) { + e.printStackTrace(System.err); + } + return false; + } + + @Override + public void start() throws IOException { + log.fine("Starting emulator via Google Cloud SDK"); + process = CommandWrapper.create().setCommand(commandText).setRedirectErrorStream().start(); + } + + @Override + public int waitFor(Duration timeout) throws InterruptedException, TimeoutException { + return waitForProcess(process, timeout); + } + + @Override + public Process getProcess() { + return process; + } + + private boolean isGcloudInstalled() { + Map env = System.getenv(); + for (String envName : env.keySet()) { + if ("PATH".equals(envName)) { + return env.get(envName).contains("google-cloud-sdk"); + } + } + return false; + } + + private boolean isEmulatorUpToDate() throws IOException, InterruptedException { + Version currentVersion = getInstalledEmulatorVersion(versionPrefix); + return currentVersion != null && currentVersion.compareTo(minVersion) >= 0; + } + + private Version getInstalledEmulatorVersion(String versionPrefix) + throws IOException, InterruptedException { + Process process = + CommandWrapper.create() + .setCommand(Arrays.asList("gcloud", "version")) + // gcloud redirects all output to stderr while emulators' executables use either + // stdout or stderr with no apparent convention. To be able to properly intercept and + // block waiting for emulators to be ready we redirect everything to stdout + .setRedirectErrorStream() + .start(); + process.waitFor(); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream()))) { + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + if (line.startsWith(versionPrefix)) { + String[] lineComponents = line.split(" "); + if (lineComponents.length > 1) { + return Version.fromString(lineComponents[1]); + } + } + } + return null; + } + } + } + + /** Utility class to start and run an emulator from a download URL. */ + protected static class DownloadableEmulatorRunner implements EmulatorRunner { + + private final List commandText; + private final String md5CheckSum; + private final URL downloadUrl; + private final String fileName; + private Process process; + private static final Logger log = Logger.getLogger(DownloadableEmulatorRunner.class.getName()); + + public DownloadableEmulatorRunner( + List commandText, URL downloadUrl, String md5CheckSum) { + this.commandText = commandText; + this.md5CheckSum = md5CheckSum; + this.downloadUrl = downloadUrl; + String[] splitUrl = downloadUrl.toString().split("/"); + this.fileName = splitUrl[splitUrl.length - 1]; + } + + @Override + public boolean isAvailable() { + try { + downloadZipFile(); + return true; + } catch (IOException e) { + return false; + } + } + + @Override + public void start() throws IOException { + ExceptionHandler retryOnAnythingExceptionHandler = + ExceptionHandler.newBuilder().retryOn(Exception.class).build(); + + Path emulatorPath = + RetryHelper.runWithRetries( + new Callable() { + @Override + public Path call() throws IOException { + return downloadEmulator(); + } + }, + ServiceOptions.getDefaultRetrySettings(), + retryOnAnythingExceptionHandler, + CurrentMillisClock.getDefaultClock()); + process = + CommandWrapper.create() + .setCommand(commandText) + .setDirectory(emulatorPath) + // gcloud redirects all output to stderr while emulators' executables use either + // stdout + // or stderr with no apparent convention. To be able to properly intercept and block + // waiting for emulators to be ready we redirect everything to stdout + .setRedirectErrorStream() + .start(); + } + + @Override + public int waitFor(Duration timeout) throws InterruptedException, TimeoutException { + return waitForProcess(process, timeout); + } + + @Override + public Process getProcess() { + return process; + } + + private Path downloadEmulator() throws IOException { + // Retrieve the file name from the download link + String[] splittedUrl = downloadUrl.toString().split("/"); + String fileName = splittedUrl[splittedUrl.length - 1]; + + // Each run is associated with its own folder that is deleted once test completes. + Path emulatorPath = Files.createTempDirectory(fileName.split("\\.")[0]); + File emulatorFolder = emulatorPath.toFile(); + emulatorFolder.deleteOnExit(); + + File zipFile = downloadZipFile(); + // Unzip the emulator + try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFile))) { + if (log.isLoggable(Level.FINE)) { + log.fine("Unzipping emulator"); + } + ZipEntry entry = zipIn.getNextEntry(); + while (entry != null) { + File filePath = new File(emulatorFolder, entry.getName()); + String canonicalEmulatorFolderPath = emulatorFolder.getCanonicalPath(); + String canonicalFilePath = filePath.getCanonicalPath(); + if (!canonicalFilePath.startsWith(canonicalEmulatorFolderPath + File.separator)) { + throw new IllegalStateException( + "Entry is outside of the target dir: " + entry.getName()); + } + if (!entry.isDirectory()) { + extractFile(zipIn, filePath); + } else { + filePath.mkdir(); + } + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + } + } + return emulatorPath; + } + + private File downloadZipFile() throws IOException { + // Check if we already have a local copy of the emulator and download it if not. + File zipFile = new File(System.getProperty("java.io.tmpdir"), fileName); + if (!zipFile.exists() || (md5CheckSum != null && !md5CheckSum.equals(md5(zipFile)))) { + if (log.isLoggable(Level.FINE)) { + log.fine("Fetching emulator"); + } + ReadableByteChannel rbc = Channels.newChannel(downloadUrl.openStream()); + try (FileOutputStream fos = new FileOutputStream(zipFile)) { + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + } + } else { + if (log.isLoggable(Level.FINE)) { + log.fine("Using cached emulator"); + } + } + return zipFile; + } + + private 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); + } + } + } + + private static String md5(File zipFile) throws IOException { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + try (InputStream is = new BufferedInputStream(new FileInputStream(zipFile))) { + 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); + } + } + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/testing/BlockingProcessStreamReader.java b/google-cloud-core/src/main/java/com/google/cloud/testing/BlockingProcessStreamReader.java new file mode 100644 index 0000000000..90fbb764d5 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/testing/BlockingProcessStreamReader.java @@ -0,0 +1,140 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.testing; + +import static com.google.common.base.MoreObjects.firstNonNull; + +import com.google.common.base.Strings; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class allows to read a process output stream, block until a provided string appears on the + * stream and redirect pertinent error logs to a provided logger. + */ +class BlockingProcessStreamReader extends Thread { + + private static final int LOG_LENGTH_LIMIT = 50000; + + private final BufferedReader errorReader; + private final Logger logger; + private StringBuilder currentLog; + private Level currentLogLevel; + private boolean collectionMode; + private final String emulatorTag; + private final Pattern logLinePattern; + + private BlockingProcessStreamReader( + String emulator, InputStream stream, String blockUntil, Logger logger) throws IOException { + super("blocking-process-stream-reader"); + setDaemon(true); + errorReader = new BufferedReader(new InputStreamReader(stream)); + this.logger = logger; + this.emulatorTag = "[" + emulator + "]"; + this.logLinePattern = Pattern.compile("(\\[" + emulator + "\\]\\s)?(\\w+):.*"); + if (!Strings.isNullOrEmpty(blockUntil)) { + String line; + do { + line = errorReader.readLine(); + } while (line != null && !line.contains(blockUntil)); + } + } + + @Override + public void run() { + String previousLine = ""; + String nextLine = ""; + try { + for (; ; ) { + previousLine = nextLine; + nextLine = errorReader.readLine(); + if (nextLine == null) { + break; + } + processLogLine(previousLine, nextLine); + } + } catch (IOException e) { + e.printStackTrace(System.err); + } + processLogLine(previousLine, firstNonNull(nextLine, "")); + writeLog(); + } + + private void processLogLine(String previousLine, String nextLine) { + // Each log is two lines with the following format: + // [Emulator]? [Date] [Time] [LoggingClass] [method] + // [Emulator]? [LEVEL]: error message + // [Emulator]? more data + // Exceptions and stack traces are included in error stream, separated by a newline + Level nextLogLevel = getLevel(nextLine); + if (nextLogLevel != null) { + writeLog(); + currentLog = new StringBuilder(); + currentLogLevel = nextLogLevel; + collectionMode = true; + } else if (collectionMode) { + if (currentLog.length() > LOG_LENGTH_LIMIT) { + collectionMode = false; + } else if (currentLog.length() == 0) { + // strip level out of the line + currentLog.append(emulatorTag); + currentLog.append(previousLine.split(":", 2)[1]); + currentLog.append(System.getProperty("line.separator")); + } else { + if (!previousLine.startsWith(emulatorTag)) { + currentLog.append(emulatorTag); + currentLog.append(' '); + } + currentLog.append(previousLine); + currentLog.append(System.getProperty("line.separator")); + } + } + } + + private void writeLog() { + if (currentLogLevel != null && currentLog != null && currentLog.length() != 0) { + logger.log(currentLogLevel, currentLog.toString().trim()); + } + } + + private Level getLevel(String line) { + try { + Matcher matcher = logLinePattern.matcher(line); + if (matcher.matches()) { + return Level.parse(matcher.group(2)); + } else { + return null; + } + } catch (IllegalArgumentException e) { + return null; // level wasn't supplied in this log line + } + } + + static BlockingProcessStreamReader start( + String emulator, InputStream stream, String blockUntil, Logger logger) throws IOException { + BlockingProcessStreamReader thread = + new BlockingProcessStreamReader(emulator, stream, blockUntil, logger); + thread.start(); + return thread; + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/testing/CommandWrapper.java b/google-cloud-core/src/main/java/com/google/cloud/testing/CommandWrapper.java new file mode 100644 index 0000000000..79f70150a5 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/testing/CommandWrapper.java @@ -0,0 +1,99 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.testing; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** Utility class that executes system commands on both Windows and Unix. */ +class CommandWrapper { + + private final List prefix; + private List command; + private String nullFilename; + private boolean redirectOutputToNull; + private boolean redirectErrorStream; + private boolean redirectErrorInherit; + private Path directory; + + private CommandWrapper() { + this.prefix = new ArrayList<>(); + if (BaseEmulatorHelper.isWindows()) { + this.prefix.add("cmd"); + this.prefix.add("/C"); + this.nullFilename = "NUL:"; + } else { + this.prefix.add("bash"); + this.nullFilename = "/dev/null"; + } + } + + CommandWrapper setCommand(List command) { + this.command = new ArrayList<>(command.size() + this.prefix.size()); + this.command.addAll(prefix); + this.command.addAll(command); + return this; + } + + CommandWrapper setRedirectOutputToNull() { + this.redirectOutputToNull = true; + return this; + } + + CommandWrapper setRedirectErrorStream() { + this.redirectErrorStream = true; + return this; + } + + CommandWrapper setRedirectErrorInherit() { + this.redirectErrorInherit = true; + return this; + } + + CommandWrapper setDirectory(Path directory) { + this.directory = directory; + return this; + } + + ProcessBuilder getBuilder() { + ProcessBuilder builder = new ProcessBuilder(command); + if (redirectOutputToNull) { + builder.redirectOutput(new File(nullFilename)); + } + if (directory != null) { + builder.directory(directory.toFile()); + } + if (redirectErrorStream) { + builder.redirectErrorStream(true); + } + if (redirectErrorInherit) { + builder.redirectError(ProcessBuilder.Redirect.INHERIT); + } + return builder; + } + + public Process start() throws IOException { + return getBuilder().start(); + } + + static CommandWrapper create() { + return new CommandWrapper(); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/testing/Version.java b/google-cloud-core/src/main/java/com/google/cloud/testing/Version.java new file mode 100644 index 0000000000..dfdc076fdf --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/testing/Version.java @@ -0,0 +1,89 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.testing; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Simplified wrapper for emulator's versions. */ +class Version implements Comparable { + + private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)$"); + + private final int major; + private final int minor; + private final int patch; + + private Version(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + @Override + public int compareTo(Version version) { + int result = major - version.major; + if (result == 0) { + result = minor - version.minor; + if (result == 0) { + result = patch - version.patch; + } + } + return result; + } + + @Override + public String toString() { + return String.format("%d.%d.%d", major, minor, patch); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof Version && compareTo((Version) other) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(major, minor, patch); + } + + int getMajor() { + return major; + } + + int getMinor() { + return minor; + } + + int getPatch() { + return patch; + } + + static Version fromString(String version) { + Matcher matcher = VERSION_PATTERN.matcher(checkNotNull(version)); + if (matcher.matches()) { + return new Version( + Integer.valueOf(matcher.group(1)), + Integer.valueOf(matcher.group(2)), + Integer.valueOf(matcher.group(3))); + } + throw new IllegalArgumentException("Invalid version format"); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/AsyncPageImplTest.java b/google-cloud-core/src/test/java/com/google/cloud/AsyncPageImplTest.java new file mode 100644 index 0000000000..241793b3be --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/AsyncPageImplTest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static org.junit.Assert.assertEquals; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.gax.paging.AsyncPage; +import com.google.common.collect.ImmutableList; +import java.util.concurrent.ExecutionException; +import org.junit.Test; + +public class AsyncPageImplTest { + + private static final ImmutableList VALUES1 = ImmutableList.of("1", "2"); + private static final ImmutableList VALUES2 = ImmutableList.of("3", "4"); + private static final ImmutableList VALUES3 = ImmutableList.of("5", "6"); + private static final ImmutableList ALL_VALUES = + ImmutableList.builder().addAll(VALUES1).addAll(VALUES2).addAll(VALUES3).build(); + private static final ImmutableList SOME_VALUES = + ImmutableList.builder().addAll(VALUES2).addAll(VALUES3).build(); + + private static class TestPageFetcher implements AsyncPageImpl.NextPageFetcher { + private static final long serialVersionUID = 4703765400378593176L; + + private final AsyncPageImpl nextResult; + + TestPageFetcher(AsyncPageImpl nextResult) { + this.nextResult = nextResult; + } + + @Override + public ApiFuture> getNextPage() { + return ApiFutures.>immediateFuture(nextResult); + } + } + + @Test + public void testPage() { + final AsyncPageImpl nextResult = new AsyncPageImpl<>(null, "c", VALUES2); + AsyncPageImpl.NextPageFetcher fetcher = new TestPageFetcher(nextResult); + AsyncPageImpl result = new AsyncPageImpl<>(fetcher, "c", VALUES1); + assertEquals(nextResult, result.getNextPage()); + assertEquals("c", result.getNextPageToken()); + assertEquals(VALUES1, result.getValues()); + } + + @Test + public void testPageAsync() throws ExecutionException, InterruptedException { + final AsyncPageImpl nextResult = new AsyncPageImpl<>(null, "c", VALUES2); + AsyncPageImpl.NextPageFetcher fetcher = new TestPageFetcher(nextResult); + AsyncPageImpl result = new AsyncPageImpl<>(fetcher, "c", VALUES1); + assertEquals(nextResult, result.getNextPageAsync().get()); + assertEquals("c", result.getNextPageToken()); + assertEquals(VALUES1, result.getValues()); + } + + @Test + public void testIterateAll() { + final AsyncPageImpl nextResult2 = new AsyncPageImpl<>(null, "c3", VALUES3); + AsyncPageImpl.NextPageFetcher fetcher2 = new TestPageFetcher(nextResult2); + final AsyncPageImpl nextResult1 = new AsyncPageImpl<>(fetcher2, "c2", VALUES2); + AsyncPageImpl.NextPageFetcher fetcher1 = new TestPageFetcher(nextResult1); + AsyncPageImpl result = new AsyncPageImpl<>(fetcher1, "c1", VALUES1); + assertEquals(ALL_VALUES, ImmutableList.copyOf(result.iterateAll())); + } + + @Test + public void testAsyncPageAndIterateAll() throws ExecutionException, InterruptedException { + final AsyncPageImpl nextResult2 = new AsyncPageImpl<>(null, "c3", VALUES3); + AsyncPageImpl.NextPageFetcher fetcher2 = new TestPageFetcher(nextResult2); + final AsyncPageImpl nextResult1 = new AsyncPageImpl<>(fetcher2, "c2", VALUES2); + AsyncPageImpl.NextPageFetcher fetcher1 = new TestPageFetcher(nextResult1); + AsyncPageImpl result = new AsyncPageImpl<>(fetcher1, "c1", VALUES1); + assertEquals(nextResult1, result.getNextPageAsync().get()); + assertEquals("c1", result.getNextPageToken()); + assertEquals(VALUES1, result.getValues()); + assertEquals(SOME_VALUES, ImmutableList.copyOf(result.getNextPageAsync().get().iterateAll())); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/BaseSerializationTest.java b/google-cloud-core/src/test/java/com/google/cloud/BaseSerializationTest.java new file mode 100644 index 0000000000..5dcd1726bb --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/BaseSerializationTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + +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 org.junit.Test; + +/** + * Base class for serialization tests. To use this class in your tests override the {@code + * serializableObjects()} method to return all objects that must be serializable. Also override + * {@code restorableObjects()} method to return all restorable objects whose state must be tested + * for proper serialization. Both methods can return {@code null} if no such object needs to be + * tested. + */ +public abstract class BaseSerializationTest { + + /** Returns all objects for which correct serialization must be tested. */ + protected abstract Serializable[] serializableObjects(); + + /** Returns all restorable objects whose state must be tested for proper serialization. */ + protected abstract Restorable[] restorableObjects(); + + @Test + public void testSerializableObjects() throws Exception { + for (Serializable obj : firstNonNull(serializableObjects(), new Serializable[0])) { + Object copy = serializeAndDeserialize(obj); + assertEquals(obj, obj); + assertEquals(obj, copy); + assertEquals(obj.hashCode(), copy.hashCode()); + assertEquals(obj.toString(), copy.toString()); + assertNotSame(obj, copy); + assertEquals(copy, copy); + } + } + + @Test + public void testRestorableObjects() throws Exception { + for (Restorable restorable : firstNonNull(restorableObjects(), new Restorable[0])) { + RestorableState state = restorable.capture(); + RestorableState deserializedState = serializeAndDeserialize(state); + assertEquals(state, deserializedState); + assertEquals(state.hashCode(), deserializedState.hashCode()); + assertEquals(state.toString(), deserializedState.toString()); + } + } + + @SuppressWarnings("unchecked") + public 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/google-cloud-core/src/test/java/com/google/cloud/BaseServiceExceptionTest.java b/google-cloud-core/src/test/java/com/google/cloud/BaseServiceExceptionTest.java new file mode 100644 index 0000000000..1f6de6007b --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/BaseServiceExceptionTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.cloud.BaseServiceException.UNKNOWN_CODE; +import static com.google.common.truth.Truth.assertThat; +import static org.easymock.EasyMock.createMock; +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.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.BaseServiceException.ExceptionData; +import com.google.common.collect.ImmutableSet; +import java.util.Set; +import org.junit.Test; + +/** Tests for {@link BaseServiceException}. */ +public class BaseServiceExceptionTest { + + private static final int CODE = 1; + private static final int CODE_NO_REASON = 2; + private static final String MESSAGE = "some message"; + private static final String REASON = "some reason"; + private static final boolean NOT_RETRYABLE = false; + private static final boolean RETRYABLE = true; + private static final boolean IDEMPOTENT = true; + private static final String DEBUG_INFO = "debugInfo"; + private static final String LOCATION = "location"; + + private static class CustomServiceException extends BaseServiceException { + + private static final long serialVersionUID = -195251309124875103L; + + public CustomServiceException(int code, String message, String reason, boolean idempotent) { + super( + ExceptionData.from( + code, + message, + reason, + BaseServiceException.isRetryable(code, reason, idempotent, RETRYABLE_ERRORS))); + } + + private static final Set RETRYABLE_ERRORS = + ImmutableSet.of( + new Error(CODE, REASON), new Error(null, REASON), new Error(CODE_NO_REASON, null)); + } + + @Test + public void testBaseServiceException() { + BaseServiceException serviceException = + new BaseServiceException(ExceptionData.from(CODE, MESSAGE, REASON, NOT_RETRYABLE)); + assertEquals(CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(REASON, serviceException.getReason()); + assertFalse(serviceException.isRetryable()); + assertNull(serviceException.getCause()); + + Exception cause = new RuntimeException(); + serviceException = + new BaseServiceException(ExceptionData.from(CODE, MESSAGE, REASON, NOT_RETRYABLE, cause)); + assertEquals(CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(REASON, serviceException.getReason()); + assertFalse(serviceException.isRetryable()); + assertEquals(cause, serviceException.getCause()); + + serviceException = + new BaseServiceException( + ExceptionData.newBuilder() + .setMessage(MESSAGE) + .setCause(cause) + .setCode(CODE) + .setReason(REASON) + .setRetryable(RETRYABLE) + .setDebugInfo(DEBUG_INFO) + .setLocation(LOCATION) + .build()); + assertEquals(CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(REASON, serviceException.getReason()); + assertTrue(serviceException.isRetryable()); + assertEquals(cause, serviceException.getCause()); + assertEquals(DEBUG_INFO, serviceException.getDebugInfo()); + assertEquals(LOCATION, serviceException.getLocation()); + + serviceException = new CustomServiceException(CODE, MESSAGE, REASON, IDEMPOTENT); + assertEquals(CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(REASON, serviceException.getReason()); + assertTrue(serviceException.isRetryable()); + + serviceException = new CustomServiceException(CODE_NO_REASON, MESSAGE, null, IDEMPOTENT); + assertEquals(CODE_NO_REASON, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertNull(serviceException.getReason()); + assertTrue(serviceException.isRetryable()); + + serviceException = new CustomServiceException(UNKNOWN_CODE, MESSAGE, REASON, IDEMPOTENT); + assertEquals(UNKNOWN_CODE, serviceException.getCode()); + assertEquals(MESSAGE, serviceException.getMessage()); + assertEquals(REASON, serviceException.getReason()); + assertTrue(serviceException.isRetryable()); + } + + @Test + public void testTranslateAndThrow() throws Exception { + BaseServiceException cause = + new BaseServiceException(ExceptionData.from(CODE, MESSAGE, REASON, NOT_RETRYABLE)); + RetryHelper.RetryHelperException exceptionMock = + createMock(RetryHelper.RetryHelperException.class); + expect(exceptionMock.getCause()).andReturn(cause).times(2); + replay(exceptionMock); + try { + BaseServiceException.translate(exceptionMock); + } catch (BaseServiceException ex) { + assertEquals(CODE, ex.getCode()); + assertEquals(MESSAGE, ex.getMessage()); + assertFalse(ex.isRetryable()); + } finally { + verify(exceptionMock); + } + } + + @Test + public void testError_Equal() { + BaseServiceException.Error error = new BaseServiceException.Error(0, "reason", true); + assertThat(error).isEqualTo(error); + assertThat(error.hashCode()).isEqualTo(error.hashCode()); + + BaseServiceException.Error sameError = new BaseServiceException.Error(0, "reason", true); + assertThat(error).isEqualTo(sameError); + assertThat(error.hashCode()).isEqualTo(sameError.hashCode()); + + BaseServiceException.Error error2 = new BaseServiceException.Error(1, "other reason", false); + assertThat(error).isNotEqualTo(error2); + assertThat(error.hashCode()).isNotEqualTo(error2.hashCode()); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/BaseWriteChannelTest.java b/google-cloud-core/src/test/java/com/google/cloud/BaseWriteChannelTest.java new file mode 100644 index 0000000000..b5e5273420 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/BaseWriteChannelTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import com.google.cloud.spi.ServiceRpcFactory; +import java.io.IOException; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.util.Arrays; +import java.util.Random; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class BaseWriteChannelTest { + + private abstract static class CustomService implements Service {} + + private abstract static class CustomServiceOptions + extends ServiceOptions { + + private static final long serialVersionUID = 3302358029307467197L; + + protected CustomServiceOptions( + Class> serviceFactoryClass, + Class> rpcFactoryClass, + Builder builder) { + super(serviceFactoryClass, rpcFactoryClass, builder, null); + } + } + + private static final Serializable ENTITY = 42L; + private static final String UPLOAD_ID = "uploadId"; + private static final byte[] CONTENT = {0xD, 0xE, 0xA, 0xD}; + private static final int MIN_CHUNK_SIZE = 256 * 1024; + private static final int DEFAULT_CHUNK_SIZE = 8 * MIN_CHUNK_SIZE; + private static final Random RANDOM = new Random(); + private static BaseWriteChannel channel; + + @Rule public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() { + channel = + new BaseWriteChannel(null, ENTITY, UPLOAD_ID) { + @Override + public RestorableState capture() { + return null; + } + + @Override + protected void flushBuffer(int length, boolean last) {} + + @Override + protected BaseState.Builder stateBuilder() { + return null; + } + }; + } + + @Test + public void testConstructor() { + assertEquals(null, channel.getOptions()); + assertEquals(ENTITY, channel.getEntity()); + assertEquals(0, channel.getPosition()); + assertEquals(UPLOAD_ID, channel.getUploadId()); + assertEquals(0, channel.getLimit()); + assertTrue(channel.isOpen()); + assertArrayEquals(new byte[0], channel.getBuffer()); + assertEquals(DEFAULT_CHUNK_SIZE, channel.getChunkSize()); + } + + @Test + public void testClose() throws IOException { + channel.close(); + assertFalse(channel.isOpen()); + assertNull(channel.getBuffer()); + } + + @Test + public void testValidateOpen() throws IOException { + channel.close(); + thrown.expect(ClosedChannelException.class); + channel.write(ByteBuffer.allocate(42)); + } + + @Test + public void testChunkSize() { + channel.setChunkSize(42); + assertThat(channel.getChunkSize() >= MIN_CHUNK_SIZE).isTrue(); + assertThat(channel.getChunkSize() % MIN_CHUNK_SIZE).isEqualTo(0); + + channel.setChunkSize(2 * MIN_CHUNK_SIZE); + assertThat(channel.getChunkSize() >= MIN_CHUNK_SIZE).isTrue(); + assertThat(channel.getChunkSize() % MIN_CHUNK_SIZE).isEqualTo(0); + + channel.setChunkSize(2 * MIN_CHUNK_SIZE + 1); + assertThat(channel.getChunkSize() >= MIN_CHUNK_SIZE).isTrue(); + assertThat(channel.getChunkSize() % MIN_CHUNK_SIZE).isEqualTo(0); + } + + @Test + public void testWrite() throws IOException { + channel.write(ByteBuffer.wrap(CONTENT)); + assertEquals(CONTENT.length, channel.getLimit()); + assertEquals(DEFAULT_CHUNK_SIZE, channel.getBuffer().length); + assertArrayEquals(Arrays.copyOf(CONTENT, DEFAULT_CHUNK_SIZE), channel.getBuffer()); + } + + @Test + public void testWriteAndFlush() throws IOException { + ByteBuffer content = randomBuffer(DEFAULT_CHUNK_SIZE + 1); + channel.write(content); + assertEquals(DEFAULT_CHUNK_SIZE, channel.getPosition()); + assertEquals(1, channel.getLimit()); + byte[] newContent = new byte[DEFAULT_CHUNK_SIZE]; + newContent[0] = content.get(DEFAULT_CHUNK_SIZE); + assertArrayEquals(newContent, channel.getBuffer()); + } + + private static ByteBuffer randomBuffer(int size) { + byte[] byteArray = new byte[size]; + RANDOM.nextBytes(byteArray); + return ByteBuffer.wrap(byteArray); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/BatchResultTest.java b/google-cloud-core/src/test/java/com/google/cloud/BatchResultTest.java new file mode 100644 index 0000000000..001bba9834 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/BatchResultTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.cloud.BaseServiceException.ExceptionData; +import org.easymock.EasyMock; +import org.junit.Before; +import org.junit.Test; + +public class BatchResultTest { + + private BatchResult result; + + @Before + public void setUp() { + result = new BatchResult() {}; + } + + @Test + public void testSuccess() { + assertFalse(result.completed()); + try { + result.get(); + fail("This was not completed yet."); + } catch (IllegalStateException ex) { + // expected + } + result.success(true); + assertTrue(result.get()); + // test that null is allowed + result.success(null); + } + + @Test + public void testError() { + assertFalse(result.completed()); + try { + result.get(); + fail("This was not completed yet."); + } catch (IllegalStateException ex) { + // expected + } + try { + result.error(null); + fail(); + } catch (NullPointerException exc) { + // expected + } + BaseServiceException ex = + new BaseServiceException(ExceptionData.from(0, "message", "reason", false)); + result.error(ex); + try { + result.get(); + fail("This is a failed operation and should have thrown a DnsException."); + } catch (BaseServiceException real) { + assertSame(ex, real); + } + } + + @Test + public void testNotifyError() { + final BaseServiceException ex = + new BaseServiceException(ExceptionData.from(0, "message", "reason", false)); + assertFalse(result.completed()); + BatchResult.Callback callback = + EasyMock.createStrictMock(BatchResult.Callback.class); + callback.error(ex); + EasyMock.replay(callback); + result.notify(callback); + result.error(ex); + try { + result.notify(callback); + fail("The batch has been completed."); + } catch (IllegalStateException exception) { + // expected + } + EasyMock.verify(callback); + } + + @Test + public void testNotifySuccess() { + assertFalse(result.completed()); + BatchResult.Callback callback = + EasyMock.createStrictMock(BatchResult.Callback.class); + callback.success(true); + EasyMock.replay(callback); + result.notify(callback); + result.success(true); + try { + result.notify(callback); + fail("The batch has been completed."); + } catch (IllegalStateException exception) { + // expected + } + EasyMock.verify(callback); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/ByteArrayTest.java b/google-cloud-core/src/test/java/com/google/cloud/ByteArrayTest.java new file mode 100644 index 0000000000..42873f8643 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/ByteArrayTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import com.google.common.io.ByteStreams; +import com.google.protobuf.ByteString; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import org.junit.BeforeClass; +import org.junit.Test; + +public class ByteArrayTest { + + private static final String STRING_CONTENT = "Hello, ByteArray!"; + private static final byte[] BYTES_CONTENT = STRING_CONTENT.getBytes(StandardCharsets.UTF_8); + private static final ByteBuffer BYTE_BUFFER_CONTENT = ByteBuffer.wrap(BYTES_CONTENT); + private static final InputStream STREAM_CONTENT = new ByteArrayInputStream(BYTES_CONTENT); + private static final ByteArray STRING_ARRAY = ByteArray.copyFrom(STRING_CONTENT); + private static final ByteArray BYTES_ARRAY = ByteArray.copyFrom(BYTES_CONTENT); + private static final ByteArray BYTE_BUFFER_ARRAY = ByteArray.copyFrom(BYTE_BUFFER_CONTENT); + private static final ByteArray ARRAY = new ByteArray(ByteString.copyFrom(BYTES_CONTENT)); + + private static ByteArray streamArray; + + @BeforeClass + public static void beforeClass() throws IOException { + streamArray = ByteArray.copyFrom(STREAM_CONTENT); + BYTE_BUFFER_CONTENT.flip(); + } + + @Test + public void testCopyFromString() throws IOException { + assertEquals(STRING_CONTENT, STRING_ARRAY.toStringUtf8()); + assertArrayEquals(BYTES_CONTENT, STRING_ARRAY.toByteArray()); + assertEquals(BYTE_BUFFER_CONTENT.asReadOnlyBuffer(), STRING_ARRAY.asReadOnlyByteBuffer()); + assertArrayEquals(BYTES_CONTENT, ByteStreams.toByteArray(STRING_ARRAY.asInputStream())); + } + + @Test + public void testCopyFromByteArray() throws IOException { + assertEquals(STRING_CONTENT, BYTES_ARRAY.toStringUtf8()); + assertArrayEquals(BYTES_CONTENT, BYTES_ARRAY.toByteArray()); + assertEquals(BYTE_BUFFER_CONTENT.asReadOnlyBuffer(), BYTES_ARRAY.asReadOnlyByteBuffer()); + assertArrayEquals(BYTES_CONTENT, ByteStreams.toByteArray(BYTES_ARRAY.asInputStream())); + } + + @Test + public void testCopyFromByteBuffer() throws IOException { + assertEquals(STRING_CONTENT, BYTE_BUFFER_ARRAY.toStringUtf8()); + assertArrayEquals(BYTES_CONTENT, BYTE_BUFFER_ARRAY.toByteArray()); + assertEquals(BYTE_BUFFER_CONTENT.asReadOnlyBuffer(), BYTE_BUFFER_ARRAY.asReadOnlyByteBuffer()); + assertArrayEquals(BYTES_CONTENT, ByteStreams.toByteArray(BYTE_BUFFER_ARRAY.asInputStream())); + } + + @Test + public void testCopyFromStream() throws IOException { + assertEquals(STRING_CONTENT, streamArray.toStringUtf8()); + assertArrayEquals(BYTES_CONTENT, streamArray.toByteArray()); + assertEquals(BYTE_BUFFER_CONTENT.asReadOnlyBuffer(), streamArray.asReadOnlyByteBuffer()); + assertArrayEquals(BYTES_CONTENT, ByteStreams.toByteArray(streamArray.asInputStream())); + } + + @Test + public void testLength() { + assertEquals(BYTES_CONTENT.length, ARRAY.length()); + } + + @Test + public void testToStringUtf8() { + assertEquals(STRING_CONTENT, ARRAY.toStringUtf8()); + } + + @Test + public void testToByteArray() { + assertArrayEquals(BYTES_CONTENT, ARRAY.toByteArray()); + } + + @Test + public void testAsReadOnlyByteBuffer() { + assertEquals(BYTE_BUFFER_CONTENT.asReadOnlyBuffer(), ARRAY.asReadOnlyByteBuffer()); + } + + @Test + public void testAsInputStream() throws IOException { + assertArrayEquals(BYTES_CONTENT, ByteStreams.toByteArray(ARRAY.asInputStream())); + } + + @Test + public void testHashCode() { + assertEquals(STRING_ARRAY.hashCode(), BYTES_ARRAY.hashCode()); + assertEquals(BYTES_ARRAY.hashCode(), BYTE_BUFFER_ARRAY.hashCode()); + assertEquals(BYTE_BUFFER_ARRAY.hashCode(), streamArray.hashCode()); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/DateTest.java b/google-cloud-core/src/test/java/com/google/cloud/DateTest.java new file mode 100644 index 0000000000..14b6a139d0 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/DateTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.testing.SerializableTester.reserializeAndAssert; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.testing.EqualsTester; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link Date}. */ +@RunWith(JUnit4.class) +public class DateTest { + + private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + + @Test + public void parseDate() { + verifyDate("2016-09-18", 2016, 9, 18); + verifyDate("2000-01-01", 2000, 1, 1); + verifyDate("9999-12-31", 9999, 12, 31); + verifyDate("0001-01-01", 1, 1, 1); + verifyDate("2000-02-29", 2000, 2, 29); // This is a valid leap year. + verifyDate("1900-02-29", 1900, 2, 29); // This is NOT a valid leap year. + verifyDate("2001-02-29", 2001, 2, 29); // Also not a valid leap year. + verifyDate("2000-04-31", 2000, 4, 31); // Not a valid date. + } + + private void verifyDate(String input, int year, int month, int day) { + Date date = Date.parseDate(input); + assertThat(date.getYear()).isEqualTo(year); + assertThat(date.getMonth()).isEqualTo(month); + assertThat(date.getDayOfMonth()).isEqualTo(day); + } + + @Test + public void parseInvalidDates() { + parseInvalidDate("2016/09/18"); + parseInvalidDate("2016 09 18"); + parseInvalidDate("2016-9-18"); + parseInvalidDate("2016-09-18T10:00"); + parseInvalidDate(""); + parseInvalidDate("test"); + parseInvalidDate("aaaa-bb-cc"); + parseInvalidDate("aaaa-01-01"); + parseInvalidDate("2019-bb-01"); + parseInvalidDate("2019-01-cc"); + parseInvalidMonth("2000-13-01"); + parseInvalidMonth("2000-00-01"); + parseInvalidDay("2000-12-32"); + parseInvalidDay("2000-12-00"); + parseInvalidDate("10000-01-01"); + parseInvalidYear("0000-01-01"); + parseInvalidYear("-001-01-01"); + parseInvalidMonth("0001--1-01"); + parseInvalidDay("0001-01--1"); + } + + private void parseInvalidDate(String input) { + try { + Date.parseDate(input); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("Invalid date"); + } + } + + private void parseInvalidYear(String input) { + try { + Date.parseDate(input); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("Invalid year"); + } + } + + private void parseInvalidMonth(String input) { + try { + Date.parseDate(input); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("Invalid month"); + } + } + + private void parseInvalidDay(String input) { + try { + Date.parseDate(input); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("Invalid day"); + } + } + + @Test + public void testToString() { + Date date = Date.fromYearMonthDay(10, 9, 5); + assertThat(date.toString()).isEqualTo("0010-09-05"); + date = Date.fromYearMonthDay(2016, 12, 31); + assertThat(date.toString()).isEqualTo("2016-12-31"); + date = Date.fromYearMonthDay(1, 1, 1); + assertThat(date.toString()).isEqualTo("0001-01-01"); + } + + @Test + public void equalAndHashCode() { + Date d1 = Date.fromYearMonthDay(2016, 9, 18); + Date d2 = Date.fromYearMonthDay(2016, 9, 18); + Date d3 = Date.fromYearMonthDay(2016, 9, 19); + EqualsTester tester = new EqualsTester(); + tester.addEqualityGroup(d1, d2); + tester.addEqualityGroup(d3); + } + + @Test + public void validOrdering() { + Date d1 = Date.fromYearMonthDay(2016, 9, 18); + Date d2 = Date.fromYearMonthDay(2016, 9, 19); + Date d3 = Date.fromYearMonthDay(2016, 9, 20); + Date d4 = Date.fromYearMonthDay(2016, 10, 1); + Date d5 = Date.fromYearMonthDay(2017, 1, 1); + assertDescending(d5, d4, d3, d2, d1); + } + + @Test + public void serialization() { + reserializeAndAssert(Date.fromYearMonthDay(2017, 4, 20)); + } + + @Test + public void testToJavaUtilDate() throws ParseException { + Date gcDate = Date.parseDate("2016-09-18"); + java.util.Date juDate1 = SIMPLE_DATE_FORMAT.parse("2016-09-18"); + java.util.Date juDate2 = Date.toJavaUtilDate(gcDate); + assertThat(juDate1).isEqualTo(juDate2); + } + + @Test + public void testFromJavaUtilDate() throws ParseException { + java.util.Date juDate = SIMPLE_DATE_FORMAT.parse("2016-09-18"); + Date gcDate = Date.fromJavaUtilDate(juDate); + assertThat(gcDate.getYear()).isEqualTo(2016); + assertThat(gcDate.getMonth()).isEqualTo(9); + assertThat(gcDate.getDayOfMonth()).isEqualTo(18); + } + + private void assertDescending(Date... dates) { + for (int i = 0; i < dates.length - 1; i++) { + assertThat(dates[i]).isEquivalentAccordingToCompareTo(dates[i]); + for (int j = i + 1; j < dates.length; j++) { + assertThat(dates[i]).isGreaterThan(dates[j]); + } + } + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/ExceptionHandlerTest.java b/google-cloud-core/src/test/java/com/google/cloud/ExceptionHandlerTest.java new file mode 100644 index 0000000000..96687cac33 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/ExceptionHandlerTest.java @@ -0,0 +1,216 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.cloud.ExceptionHandler.Interceptor; +import com.google.cloud.ExceptionHandler.Interceptor.RetryResult; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.channels.ClosedByInterruptException; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** Tests for {@link ExceptionHandler}. */ +public class ExceptionHandlerTest { + + @Rule public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testVerifyCaller() { + class A implements Callable { + @Override + public Object call() throws IOException, InterruptedException { + return null; + } + } + + class B extends A {} + + class C extends A { + @Override + public Object call() throws FileNotFoundException { + return "c"; + } + } + + class D extends C { + @Override + public Object call() throws IllegalArgumentException { + return "d"; + } + } + + class E extends A { + @Override + public String call() throws NullPointerException { + return "e"; + } + } + + class F extends A { + @Override + public Object call() throws Error { + return "f"; + } + } + + // using default exception handler (retry upon any non-runtime exceptions) + ExceptionHandler handler = ExceptionHandler.getDefaultInstance(); + assertValidCallable(new A(), handler); + assertValidCallable(new B(), handler); + assertValidCallable(new C(), handler); + assertValidCallable(new D(), handler); + assertValidCallable(new E(), handler); + assertInvalidCallable(new F(), handler); + + handler = + ExceptionHandler.newBuilder() + .retryOn(FileNotFoundException.class, NullPointerException.class) + .build(); + assertInvalidCallable(new A(), handler); + assertInvalidCallable(new B(), handler); + assertValidCallable(new C(), handler); + assertInvalidCallable(new D(), handler); + assertValidCallable(new E(), handler); + assertInvalidCallable(new F(), handler); + } + + private static void assertValidCallable(Callable callable, ExceptionHandler handler) { + handler.verifyCaller(callable); + } + + private static void assertInvalidCallable(Callable callable, ExceptionHandler handler) { + try { + handler.verifyCaller(callable); + fail("Expected RetryHelper constructor to fail"); + } catch (IllegalArgumentException ex) { + // expected + } + } + + @Test + public void testShouldTry() { + ExceptionHandler handler = ExceptionHandler.newBuilder().retryOn(IOException.class).build(); + assertTrue(handler.shouldRetry(new IOException(), null)); + assertTrue(handler.shouldRetry(new ClosedByInterruptException(), null)); + assertFalse(handler.shouldRetry(new RuntimeException(), null)); + + ExceptionHandler.Builder builder = + ExceptionHandler.newBuilder() + .retryOn(IOException.class, NullPointerException.class) + .abortOn( + RuntimeException.class, + ClosedByInterruptException.class, + InterruptedException.class); + + handler = builder.build(); + assertTrue(handler.shouldRetry(new IOException(), null)); + assertFalse(handler.shouldRetry(new ClosedByInterruptException(), null)); + assertFalse(handler.shouldRetry(new InterruptedException(), null)); + assertFalse(handler.shouldRetry(new RuntimeException(), null)); + assertTrue(handler.shouldRetry(new NullPointerException(), null)); + + final AtomicReference before = new AtomicReference<>(RetryResult.NO_RETRY); + @SuppressWarnings("serial") + Interceptor interceptor = + new Interceptor() { + + @Override + public RetryResult afterEval(Exception exception, RetryResult retryResult) { + return retryResult == RetryResult.NO_RETRY ? RetryResult.RETRY : RetryResult.NO_RETRY; + } + + @Override + public RetryResult beforeEval(Exception exception) { + return before.get(); + } + }; + + builder.addInterceptors(interceptor); + handler = builder.build(); + assertFalse(handler.shouldRetry(new IOException(), null)); + assertFalse(handler.shouldRetry(new ClosedByInterruptException(), null)); + assertFalse(handler.shouldRetry(new InterruptedException(), null)); + assertFalse(handler.shouldRetry(new RuntimeException(), null)); + assertFalse(handler.shouldRetry(new NullPointerException(), null)); + + before.set(RetryResult.RETRY); + assertTrue(handler.shouldRetry(new IOException(), null)); + assertTrue(handler.shouldRetry(new ClosedByInterruptException(), null)); + assertTrue(handler.shouldRetry(new InterruptedException(), null)); + assertTrue(handler.shouldRetry(new RuntimeException(), null)); + assertTrue(handler.shouldRetry(new NullPointerException(), null)); + + before.set(RetryResult.CONTINUE_EVALUATION); + assertFalse(handler.shouldRetry(new IOException(), null)); + assertTrue(handler.shouldRetry(new ClosedByInterruptException(), null)); + assertTrue(handler.shouldRetry(new InterruptedException(), null)); + assertTrue(handler.shouldRetry(new RuntimeException(), null)); + assertFalse(handler.shouldRetry(new NullPointerException(), null)); + } + + @Test + public void testNullRetryResultFromBeforeEval() { + @SuppressWarnings("serial") + Interceptor interceptor = + new Interceptor() { + + @Override + public RetryResult beforeEval(Exception exception) { + return null; + } + + @Override + public RetryResult afterEval(Exception exception, RetryResult retryResult) { + return RetryResult.CONTINUE_EVALUATION; + } + }; + + ExceptionHandler handler = ExceptionHandler.newBuilder().addInterceptors(interceptor).build(); + thrown.expect(NullPointerException.class); + handler.shouldRetry(new Exception(), null); + } + + @Test + public void testNullRetryResultFromAfterEval() { + @SuppressWarnings("serial") + Interceptor interceptor = + new Interceptor() { + + @Override + public RetryResult beforeEval(Exception exception) { + return RetryResult.CONTINUE_EVALUATION; + } + + @Override + public RetryResult afterEval(Exception exception, RetryResult retryResult) { + return null; + } + }; + + ExceptionHandler handler = ExceptionHandler.newBuilder().addInterceptors(interceptor).build(); + thrown.expect(NullPointerException.class); + handler.shouldRetry(new Exception(), null); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/FieldSelectorHelperTest.java b/google-cloud-core/src/test/java/com/google/cloud/FieldSelectorHelperTest.java new file mode 100644 index 0000000000..7b94ff5294 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/FieldSelectorHelperTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.FieldSelector.Helper; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; + +public class FieldSelectorHelperTest { + + private static final FieldSelector FIELD1 = + new FieldSelector() { + @Override + public String getSelector() { + return "field1"; + } + }; + private static final FieldSelector FIELD2 = + new FieldSelector() { + @Override + public String getSelector() { + return "field2"; + } + }; + private static final FieldSelector FIELD3 = + new FieldSelector() { + @Override + public String getSelector() { + return "field3"; + } + }; + private static final String[] FIRST_LEVEL_FIELDS = {"firstLevel1", "firstLevel2"}; + private static final List REQUIRED_FIELDS = ImmutableList.of(FIELD1, FIELD2); + private static final String CONTAINER = "container"; + + @Test + public void testSelector() { + String selector = Helper.selector(REQUIRED_FIELDS, FIELD3); + assertTrue(selector.contains("field1")); + assertTrue(selector.contains("field2")); + assertTrue(selector.contains("field3")); + assertEquals(20, selector.length()); + } + + @Test + public void testListSelector() { + String selector = Helper.listSelector(CONTAINER, REQUIRED_FIELDS, FIELD3); + assertTrue(selector.startsWith("nextPageToken,container(")); + assertTrue(selector.contains("field1")); + assertTrue(selector.contains("field2")); + assertTrue(selector.contains("field3")); + assertTrue(selector.endsWith(")")); + assertEquals(45, selector.length()); + } + + @Test + public void testListSelectorWithExtraFields() { + String selector = + Helper.listSelector(CONTAINER, REQUIRED_FIELDS, new FieldSelector[] {FIELD3}, "field4"); + assertTrue(selector.startsWith("nextPageToken,container(")); + assertTrue(selector.contains("field1")); + assertTrue(selector.contains("field2")); + assertTrue(selector.contains("field3")); + assertTrue(selector.contains("field4")); + assertTrue(selector.endsWith(")")); + assertEquals(52, selector.length()); + } + + @Test + public void testListSelectorWithFirstLevelFields() { + String selector = + Helper.listSelector( + FIRST_LEVEL_FIELDS, CONTAINER, REQUIRED_FIELDS, new FieldSelector[] {FIELD3}, "field4"); + assertTrue(selector.contains("firstLevel1")); + assertTrue(selector.contains("firstLevel2")); + assertTrue(selector.contains("nextPageToken")); + assertTrue(selector.contains("container(")); + assertTrue(selector.contains("field1")); + assertTrue(selector.contains("field2")); + assertTrue(selector.contains("field3")); + assertTrue(selector.contains("field4")); + assertTrue(selector.endsWith(")")); + assertEquals(76, selector.length()); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/IdentityTest.java b/google-cloud-core/src/test/java/com/google/cloud/IdentityTest.java new file mode 100644 index 0000000000..8f8abb170c --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/IdentityTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +public class IdentityTest { + + private static final Identity ALL_USERS = Identity.allUsers(); + private static final Identity ALL_AUTH_USERS = Identity.allAuthenticatedUsers(); + private static final Identity USER = Identity.user("abc@gmail.com"); + private static final Identity SERVICE_ACCOUNT = + Identity.serviceAccount("service-account@gmail.com"); + private static final Identity GROUP = Identity.group("group@gmail.com"); + private static final Identity DOMAIN = Identity.domain("google.com"); + private static final Identity PROJECT_OWNER = Identity.projectOwner("my-sample-project"); + private static final Identity PROJECT_EDITOR = Identity.projectEditor("my-sample-project"); + private static final Identity PROJECT_VIEWER = Identity.projectViewer("my-sample-project"); + + @Test + public void testAllUsers() { + assertEquals(Identity.Type.ALL_USERS, ALL_USERS.getType()); + assertNull(ALL_USERS.getValue()); + } + + @Test + public void testAllAuthenticatedUsers() { + assertEquals(Identity.Type.ALL_AUTHENTICATED_USERS, ALL_AUTH_USERS.getType()); + assertNull(ALL_AUTH_USERS.getValue()); + } + + @Test + public void testUser() { + assertEquals(Identity.Type.USER, USER.getType()); + assertEquals("abc@gmail.com", USER.getValue()); + } + + @Test(expected = NullPointerException.class) + public void testUserNullEmail() { + Identity.user(null); + } + + @Test + public void testServiceAccount() { + assertEquals(Identity.Type.SERVICE_ACCOUNT, SERVICE_ACCOUNT.getType()); + assertEquals("service-account@gmail.com", SERVICE_ACCOUNT.getValue()); + } + + @Test(expected = NullPointerException.class) + public void testServiceAccountNullEmail() { + Identity.serviceAccount(null); + } + + @Test + public void testGroup() { + assertEquals(Identity.Type.GROUP, GROUP.getType()); + assertEquals("group@gmail.com", GROUP.getValue()); + } + + @Test(expected = NullPointerException.class) + public void testGroupNullEmail() { + Identity.group(null); + } + + @Test + public void testDomain() { + assertEquals(Identity.Type.DOMAIN, DOMAIN.getType()); + assertEquals("google.com", DOMAIN.getValue()); + } + + @Test(expected = NullPointerException.class) + public void testDomainNullId() { + Identity.domain(null); + } + + @Test + public void testProjectOwner() { + assertEquals(Identity.Type.PROJECT_OWNER, PROJECT_OWNER.getType()); + assertEquals("my-sample-project", PROJECT_OWNER.getValue()); + } + + @Test(expected = NullPointerException.class) + public void testProjectOwnerNullId() { + Identity.projectOwner(null); + } + + @Test + public void testProjectEditor() { + assertEquals(Identity.Type.PROJECT_EDITOR, PROJECT_EDITOR.getType()); + assertEquals("my-sample-project", PROJECT_EDITOR.getValue()); + } + + @Test(expected = NullPointerException.class) + public void testProjectEditorNullId() { + Identity.projectEditor(null); + } + + @Test + public void testProjectViewer() { + assertEquals(Identity.Type.PROJECT_VIEWER, PROJECT_VIEWER.getType()); + assertEquals("my-sample-project", PROJECT_VIEWER.getValue()); + } + + @Test(expected = NullPointerException.class) + public void testProjectViewerNullId() { + Identity.projectViewer(null); + } + + @Test + public void testIdentityToAndFromPb() { + compareIdentities(ALL_USERS, Identity.valueOf(ALL_USERS.strValue())); + compareIdentities(ALL_AUTH_USERS, Identity.valueOf(ALL_AUTH_USERS.strValue())); + compareIdentities(USER, Identity.valueOf(USER.strValue())); + compareIdentities(SERVICE_ACCOUNT, Identity.valueOf(SERVICE_ACCOUNT.strValue())); + compareIdentities(GROUP, Identity.valueOf(GROUP.strValue())); + compareIdentities(DOMAIN, Identity.valueOf(DOMAIN.strValue())); + compareIdentities(PROJECT_OWNER, Identity.valueOf(PROJECT_OWNER.strValue())); + compareIdentities(PROJECT_EDITOR, Identity.valueOf(PROJECT_EDITOR.strValue())); + compareIdentities(PROJECT_VIEWER, Identity.valueOf(PROJECT_VIEWER.strValue())); + } + + @Test(expected = IllegalArgumentException.class) + public void testValueOfEmpty() { + Identity.valueOf(""); + } + + @Test + public void testUnrecognizedToString() { + assertEquals("a:b", Identity.valueOf("a:b").strValue()); + } + + @Test + public void testValueOfThreePart() { + Identity identity = Identity.valueOf("a:b:c"); + assertEquals("A", identity.getType().name()); + assertEquals("b:c", identity.getValue()); + } + + private void compareIdentities(Identity expected, Identity actual) { + assertEquals(expected, actual); + assertEquals(expected.getType(), actual.getType()); + assertEquals(expected.getValue(), actual.getValue()); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/MonitoredResourceDescriptorTest.java b/google-cloud-core/src/test/java/com/google/cloud/MonitoredResourceDescriptorTest.java new file mode 100644 index 0000000000..b29896af48 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/MonitoredResourceDescriptorTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import com.google.cloud.MonitoredResourceDescriptor.LabelDescriptor; +import com.google.cloud.MonitoredResourceDescriptor.LabelDescriptor.ValueType; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; + +public class MonitoredResourceDescriptorTest { + + private static final LabelDescriptor BOOLEAN_LABEL = + new LabelDescriptor("booleanKey", ValueType.BOOL, "Boolean label"); + private static final LabelDescriptor STRING_LABEL = + new LabelDescriptor("stringKey", ValueType.STRING, "String label"); + private static final LabelDescriptor INT_LABEL = + new LabelDescriptor("intKey", ValueType.INT64, "Int label"); + private static final LabelDescriptor INT_LABEL_NO_DESCRIPTION = + new LabelDescriptor("intKey", ValueType.INT64, null); + private static final String TYPE = "resource_type"; + private static final String NAME = "resourceName"; + private static final String DISPLAY_NAME = "Display Name"; + private static final String DESCRIPTION = "Resource Descriptor"; + private static final List LABELS = + ImmutableList.of(BOOLEAN_LABEL, STRING_LABEL, INT_LABEL); + private static final MonitoredResourceDescriptor RESOURCE_DESCRIPTOR = + MonitoredResourceDescriptor.newBuilder(TYPE) + .setName(NAME) + .setDisplayName(DISPLAY_NAME) + .setDescription(DESCRIPTION) + .setLabels(LABELS) + .build(); + + @Test + public void testLabelDescriptor() { + assertEquals("booleanKey", BOOLEAN_LABEL.getKey()); + assertEquals(ValueType.BOOL, BOOLEAN_LABEL.getValueType()); + assertEquals("Boolean label", BOOLEAN_LABEL.getDescription()); + assertEquals("stringKey", STRING_LABEL.getKey()); + assertEquals(ValueType.STRING, STRING_LABEL.getValueType()); + assertEquals("String label", STRING_LABEL.getDescription()); + assertEquals("intKey", INT_LABEL.getKey()); + assertEquals(ValueType.INT64, INT_LABEL.getValueType()); + assertEquals("Int label", INT_LABEL.getDescription()); + assertEquals("intKey", INT_LABEL_NO_DESCRIPTION.getKey()); + assertEquals(ValueType.INT64, INT_LABEL_NO_DESCRIPTION.getValueType()); + assertNull(INT_LABEL_NO_DESCRIPTION.getDescription()); + } + + @Test + public void testBuilder() { + assertEquals(TYPE, RESOURCE_DESCRIPTOR.getType()); + assertEquals(NAME, RESOURCE_DESCRIPTOR.getName()); + assertEquals(DISPLAY_NAME, RESOURCE_DESCRIPTOR.getDisplayName()); + assertEquals(DESCRIPTION, RESOURCE_DESCRIPTOR.getDescription()); + assertEquals(LABELS, RESOURCE_DESCRIPTOR.getLabels()); + MonitoredResourceDescriptor resourceDescriptor = + MonitoredResourceDescriptor.newBuilder(TYPE).build(); + assertEquals(TYPE, resourceDescriptor.getType()); + assertNull(resourceDescriptor.getName()); + assertNull(resourceDescriptor.getDisplayName()); + assertNull(resourceDescriptor.getDescription()); + assertEquals(ImmutableList.of(), resourceDescriptor.getLabels()); + } + + @Test + public void testToAndFromPbLabelDescriptor() { + compareLabelDescriptor(BOOLEAN_LABEL, LabelDescriptor.fromPb(BOOLEAN_LABEL.toPb())); + compareLabelDescriptor(STRING_LABEL, LabelDescriptor.fromPb(STRING_LABEL.toPb())); + compareLabelDescriptor(INT_LABEL, LabelDescriptor.fromPb(INT_LABEL.toPb())); + compareLabelDescriptor( + INT_LABEL_NO_DESCRIPTION, LabelDescriptor.fromPb(INT_LABEL_NO_DESCRIPTION.toPb())); + } + + @Test + public void testToAndFromPb() { + compareResourceDescriptor( + RESOURCE_DESCRIPTOR, MonitoredResourceDescriptor.fromPb(RESOURCE_DESCRIPTOR.toPb())); + MonitoredResourceDescriptor resourceDescriptor = + MonitoredResourceDescriptor.newBuilder(TYPE).build(); + compareResourceDescriptor( + resourceDescriptor, MonitoredResourceDescriptor.fromPb(resourceDescriptor.toPb())); + } + + private void compareLabelDescriptor(LabelDescriptor expected, LabelDescriptor value) { + assertEquals(expected, value); + assertEquals(expected.getKey(), value.getKey()); + assertEquals(expected.getValueType(), value.getValueType()); + assertEquals(expected.getDescription(), value.getDescription()); + assertEquals(expected.hashCode(), value.hashCode()); + assertEquals(expected.toString(), value.toString()); + } + + private void compareResourceDescriptor( + MonitoredResourceDescriptor expected, MonitoredResourceDescriptor value) { + assertEquals(expected, value); + assertEquals(expected.getType(), value.getType()); + assertEquals(expected.getName(), value.getName()); + assertEquals(expected.getDisplayName(), value.getDisplayName()); + assertEquals(expected.getDescription(), value.getDescription()); + assertEquals(expected.getLabels(), value.getLabels()); + assertEquals(expected.hashCode(), value.hashCode()); + assertEquals(expected.toString(), value.toString()); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/MonitoredResourceTest.java b/google-cloud-core/src/test/java/com/google/cloud/MonitoredResourceTest.java new file mode 100644 index 0000000000..6054a7aa34 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/MonitoredResourceTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static org.junit.Assert.assertEquals; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import org.junit.Test; + +public class MonitoredResourceTest { + + private static final String TYPE = "cloudsql_database"; + private static final Map LABELS = + ImmutableMap.of("dataset-id", "myDataset", "zone", "myZone"); + private static final MonitoredResource MONITORED_RESOURCE = + MonitoredResource.newBuilder(TYPE).setLabels(LABELS).build(); + + @Test + public void testBuilder() { + assertEquals(TYPE, MONITORED_RESOURCE.getType()); + assertEquals(LABELS, MONITORED_RESOURCE.getLabels()); + MonitoredResource monitoredResource = + MonitoredResource.newBuilder(TYPE) + .addLabel("dataset-id", "myDataset") + .addLabel("zone", "myZone") + .build(); + assertEquals(TYPE, monitoredResource.getType()); + assertEquals(LABELS, monitoredResource.getLabels()); + compareMonitoredResource(MONITORED_RESOURCE, monitoredResource); + monitoredResource = + MonitoredResource.newBuilder(TYPE) + .setType("global") + .addLabel("dataset-id", "myDataset") + .addLabel("zone", "myZone") + .clearLabels() + .build(); + assertEquals("global", monitoredResource.getType()); + assertEquals(ImmutableMap.of(), monitoredResource.getLabels()); + } + + @Test + public void testToBuilder() { + compareMonitoredResource(MONITORED_RESOURCE, MONITORED_RESOURCE.toBuilder().build()); + MonitoredResource monitoredResource = + MONITORED_RESOURCE.toBuilder().setType("global").clearLabels().build(); + assertEquals("global", monitoredResource.getType()); + assertEquals(ImmutableMap.of(), monitoredResource.getLabels()); + monitoredResource = + monitoredResource + .toBuilder() + .setType(TYPE) + .setLabels(ImmutableMap.of("dataset-id", "myDataset")) + .addLabel("zone", "myZone") + .build(); + compareMonitoredResource(MONITORED_RESOURCE, monitoredResource); + } + + @Test + public void testOf() { + MonitoredResource monitoredResource = MonitoredResource.of(TYPE, LABELS); + assertEquals(TYPE, monitoredResource.getType()); + assertEquals(LABELS, monitoredResource.getLabels()); + compareMonitoredResource(MONITORED_RESOURCE, monitoredResource); + } + + @Test + public void testToAndFromPb() { + compareMonitoredResource( + MONITORED_RESOURCE, MonitoredResource.fromPb(MONITORED_RESOURCE.toPb())); + MonitoredResource monitoredResource = + MonitoredResource.of(TYPE, ImmutableMap.of()); + compareMonitoredResource(monitoredResource, MonitoredResource.fromPb(monitoredResource.toPb())); + } + + private void compareMonitoredResource(MonitoredResource expected, MonitoredResource value) { + assertEquals(expected, value); + assertEquals(expected.getType(), value.getType()); + assertEquals(expected.getLabels(), value.getLabels()); + assertEquals(expected.hashCode(), value.hashCode()); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/PageImplTest.java b/google-cloud-core/src/test/java/com/google/cloud/PageImplTest.java new file mode 100644 index 0000000000..5e6a6782d6 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/PageImplTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static org.junit.Assert.assertEquals; + +import com.google.api.gax.paging.Page; +import com.google.common.collect.ImmutableList; +import org.junit.Test; + +public class PageImplTest { + + private static final ImmutableList VALUES = ImmutableList.of("1", "2"); + private static final ImmutableList NEXT_VALUES = ImmutableList.of("3", "4"); + private static final ImmutableList ALL_VALUES = + ImmutableList.builder().addAll(VALUES).addAll(NEXT_VALUES).build(); + + private static class TestPageFetcher implements PageImpl.NextPageFetcher { + private static final long serialVersionUID = -8316752901403429976L; + + private final PageImpl nextResult; + + TestPageFetcher(PageImpl nextResult) { + this.nextResult = nextResult; + } + + @Override + public Page getNextPage() { + return nextResult; + } + } + + @Test + public void testPage() { + final PageImpl nextResult = new PageImpl<>(null, "c", NEXT_VALUES); + PageImpl.NextPageFetcher fetcher = new TestPageFetcher(nextResult); + PageImpl result = new PageImpl<>(fetcher, "c", VALUES); + assertEquals(nextResult, result.getNextPage()); + assertEquals("c", result.getNextPageToken()); + assertEquals(VALUES, result.getValues()); + } + + @Test + public void testIterateAll() { + final PageImpl nextResult = new PageImpl<>(null, "c", NEXT_VALUES); + PageImpl.NextPageFetcher fetcher = new TestPageFetcher(nextResult); + PageImpl result = new PageImpl<>(fetcher, "c", VALUES); + assertEquals(ALL_VALUES, ImmutableList.copyOf(result.iterateAll())); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/PolicyTest.java b/google-cloud-core/src/test/java/com/google/cloud/PolicyTest.java new file mode 100644 index 0000000000..61d99223e4 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/PolicyTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.cloud.Policy.DefaultMarshaller; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.junit.Test; + +public class PolicyTest { + + private static final Identity ALL_USERS = Identity.allUsers(); + private static final Identity ALL_AUTH_USERS = Identity.allAuthenticatedUsers(); + private static final Identity USER = Identity.user("abc@gmail.com"); + private static final Identity SERVICE_ACCOUNT = + Identity.serviceAccount("service-account@gmail.com"); + private static final Identity GROUP = Identity.group("group@gmail.com"); + private static final Identity DOMAIN = Identity.domain("google.com"); + private static final Role VIEWER = Role.viewer(); + private static final Role EDITOR = Role.editor(); + private static final Role OWNER = Role.owner(); + private static final Map> BINDINGS = + ImmutableMap.of( + VIEWER, + ImmutableSet.of(USER, SERVICE_ACCOUNT, ALL_USERS), + EDITOR, + ImmutableSet.of(ALL_AUTH_USERS, GROUP, DOMAIN)); + private static final Policy SIMPLE_POLICY = + Policy.newBuilder() + .addIdentity(VIEWER, USER, SERVICE_ACCOUNT, ALL_USERS) + .addIdentity(EDITOR, ALL_AUTH_USERS, GROUP, DOMAIN) + .build(); + private static final Policy FULL_POLICY = + Policy.newBuilder() + .setBindings(SIMPLE_POLICY.getBindings()) + .setEtag("etag") + .setVersion(1) + .build(); + + @Test + public void testBuilder() { + assertEquals(BINDINGS, SIMPLE_POLICY.getBindings()); + assertEquals(null, SIMPLE_POLICY.getEtag()); + assertEquals(0, SIMPLE_POLICY.getVersion()); + assertEquals(BINDINGS, FULL_POLICY.getBindings()); + assertEquals("etag", FULL_POLICY.getEtag()); + assertEquals(1, FULL_POLICY.getVersion()); + Map> editorBinding = + ImmutableMap.>builder().put(EDITOR, BINDINGS.get(EDITOR)).build(); + Policy policy = FULL_POLICY.toBuilder().setBindings(editorBinding).build(); + assertEquals(editorBinding, policy.getBindings()); + assertEquals("etag", policy.getEtag()); + assertEquals(1, policy.getVersion()); + policy = SIMPLE_POLICY.toBuilder().removeRole(EDITOR).build(); + assertEquals(ImmutableMap.of(VIEWER, BINDINGS.get(VIEWER)), policy.getBindings()); + assertNull(policy.getEtag()); + assertEquals(0, policy.getVersion()); + policy = + policy + .toBuilder() + .removeIdentity(VIEWER, USER, ALL_USERS) + .addIdentity(VIEWER, DOMAIN, GROUP) + .build(); + assertEquals( + ImmutableMap.of(VIEWER, ImmutableSet.of(SERVICE_ACCOUNT, DOMAIN, GROUP)), + policy.getBindings()); + assertNull(policy.getEtag()); + assertEquals(0, policy.getVersion()); + policy = + Policy.newBuilder() + .removeIdentity(VIEWER, USER) + .addIdentity(OWNER, USER, SERVICE_ACCOUNT) + .addIdentity(EDITOR, GROUP) + .removeIdentity(EDITOR, GROUP) + .build(); + assertEquals( + ImmutableMap.of(OWNER, ImmutableSet.of(USER, SERVICE_ACCOUNT)), policy.getBindings()); + assertNull(policy.getEtag()); + assertEquals(0, policy.getVersion()); + } + + @Test + public void testIllegalPolicies() { + try { + Policy.newBuilder().addIdentity(null, USER); + fail("Null role should cause exception."); + } catch (NullPointerException ex) { + assertEquals("The role cannot be null.", ex.getMessage()); + } + try { + Policy.newBuilder().addIdentity(VIEWER, null, USER); + fail("Null identity should cause exception."); + } catch (NullPointerException ex) { + assertEquals("Null identities are not permitted.", ex.getMessage()); + } + try { + Policy.newBuilder().addIdentity(VIEWER, USER, (Identity[]) null); + fail("Null identity should cause exception."); + } catch (NullPointerException ex) { + assertEquals("Null identities are not permitted.", ex.getMessage()); + } + try { + Policy.newBuilder().setBindings(null); + fail("Null bindings map should cause exception."); + } catch (NullPointerException ex) { + assertEquals("The provided map of bindings cannot be null.", ex.getMessage()); + } + try { + Map> bindings = new HashMap<>(); + bindings.put(VIEWER, null); + Policy.newBuilder().setBindings(bindings); + fail("Null set of identities should cause exception."); + } catch (NullPointerException ex) { + assertEquals("A role cannot be assigned to a null set of identities.", ex.getMessage()); + } + try { + Map> bindings = new HashMap<>(); + Set identities = new HashSet<>(); + identities.add(null); + bindings.put(VIEWER, identities); + Policy.newBuilder().setBindings(bindings); + fail("Null identity should cause exception."); + } catch (IllegalArgumentException ex) { + assertEquals("Null identities are not permitted.", ex.getMessage()); + } + } + + @Test + public void testEqualsHashCode() { + assertNotNull(FULL_POLICY); + Policy emptyPolicy = Policy.newBuilder().build(); + Policy anotherPolicy = Policy.newBuilder().build(); + assertEquals(emptyPolicy, anotherPolicy); + assertEquals(emptyPolicy.hashCode(), anotherPolicy.hashCode()); + assertNotEquals(FULL_POLICY, SIMPLE_POLICY); + assertNotEquals(FULL_POLICY.hashCode(), SIMPLE_POLICY.hashCode()); + Policy copy = SIMPLE_POLICY.toBuilder().build(); + assertEquals(SIMPLE_POLICY, copy); + assertEquals(SIMPLE_POLICY.hashCode(), copy.hashCode()); + } + + @Test + public void testBindings() { + assertTrue(Policy.newBuilder().build().getBindings().isEmpty()); + assertEquals(BINDINGS, SIMPLE_POLICY.getBindings()); + } + + @Test + public void testEtag() { + assertNull(SIMPLE_POLICY.getEtag()); + assertEquals("etag", FULL_POLICY.getEtag()); + } + + @Test + public void testVersion() { + assertEquals(0, SIMPLE_POLICY.getVersion()); + assertEquals(1, FULL_POLICY.getVersion()); + } + + @Test + public void testDefaultMarshaller() { + DefaultMarshaller marshaller = new DefaultMarshaller(); + Policy emptyPolicy = Policy.newBuilder().build(); + assertEquals(emptyPolicy, marshaller.fromPb(marshaller.toPb(emptyPolicy))); + assertEquals(SIMPLE_POLICY, marshaller.fromPb(marshaller.toPb(SIMPLE_POLICY))); + assertEquals(FULL_POLICY, marshaller.fromPb(marshaller.toPb(FULL_POLICY))); + com.google.iam.v1.Policy policyPb = com.google.iam.v1.Policy.getDefaultInstance(); + Policy policy = marshaller.fromPb(policyPb); + assertTrue(policy.getBindings().isEmpty()); + assertNull(policy.getEtag()); + assertEquals(0, policy.getVersion()); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/RetryOptionTest.java b/google-cloud-core/src/test/java/com/google/cloud/RetryOptionTest.java new file mode 100644 index 0000000000..e1c906ca23 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/RetryOptionTest.java @@ -0,0 +1,133 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import com.google.api.gax.retrying.RetrySettings; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.threeten.bp.Duration; + +public class RetryOptionTest { + @Rule public ExpectedException thrown = ExpectedException.none(); + + private static final RetryOption TOTAL_TIMEOUT = + RetryOption.totalTimeout(Duration.ofMillis(420L)); + private static final RetryOption INITIAL_RETRY_DELAY = + RetryOption.initialRetryDelay(Duration.ofMillis(42L)); + private static final RetryOption RETRY_DELAY_MULTIPLIER = RetryOption.retryDelayMultiplier(1.5); + private static final RetryOption MAX_RETRY_DELAY = + RetryOption.maxRetryDelay(Duration.ofMillis(100)); + private static final RetryOption MAX_ATTEMPTS = RetryOption.maxAttempts(100); + private static final RetryOption JITTERED = RetryOption.jittered(false); + + private static final RetrySettings retrySettings = + RetrySettings.newBuilder() + .setTotalTimeout(Duration.ofMillis(420L)) + .setInitialRetryDelay(Duration.ofMillis(42L)) + .setRetryDelayMultiplier(1.5) + .setMaxRetryDelay(Duration.ofMillis(100)) + .setMaxAttempts(100) + .setJittered(false) + .build(); + + @Test + public void testEqualsAndHashCode() { + assertEquals(TOTAL_TIMEOUT, TOTAL_TIMEOUT); + assertEquals(INITIAL_RETRY_DELAY, INITIAL_RETRY_DELAY); + assertEquals(RETRY_DELAY_MULTIPLIER, RETRY_DELAY_MULTIPLIER); + assertEquals(MAX_RETRY_DELAY, MAX_RETRY_DELAY); + assertEquals(MAX_ATTEMPTS, MAX_ATTEMPTS); + assertEquals(JITTERED, JITTERED); + + assertNotEquals(TOTAL_TIMEOUT, JITTERED); + assertNotEquals(INITIAL_RETRY_DELAY, TOTAL_TIMEOUT); + assertNotEquals(RETRY_DELAY_MULTIPLIER, INITIAL_RETRY_DELAY); + assertNotEquals(MAX_RETRY_DELAY, RETRY_DELAY_MULTIPLIER); + assertNotEquals(MAX_ATTEMPTS, MAX_RETRY_DELAY); + assertNotEquals(JITTERED, MAX_ATTEMPTS); + + RetryOption totalTimeout = RetryOption.totalTimeout(Duration.ofMillis(420L)); + RetryOption initialRetryDelay = RetryOption.initialRetryDelay(Duration.ofMillis(42L)); + RetryOption retryDelayMultiplier = RetryOption.retryDelayMultiplier(1.5); + RetryOption maxRetryDelay = RetryOption.maxRetryDelay(Duration.ofMillis(100)); + RetryOption maxAttempts = RetryOption.maxAttempts(100); + RetryOption jittered = RetryOption.jittered(false); + + assertEquals(TOTAL_TIMEOUT, totalTimeout); + assertEquals(INITIAL_RETRY_DELAY, initialRetryDelay); + assertEquals(RETRY_DELAY_MULTIPLIER, retryDelayMultiplier); + assertEquals(MAX_RETRY_DELAY, maxRetryDelay); + assertEquals(MAX_ATTEMPTS, maxAttempts); + assertEquals(JITTERED, jittered); + + assertEquals(TOTAL_TIMEOUT.hashCode(), totalTimeout.hashCode()); + assertEquals(INITIAL_RETRY_DELAY.hashCode(), initialRetryDelay.hashCode()); + assertEquals(RETRY_DELAY_MULTIPLIER.hashCode(), retryDelayMultiplier.hashCode()); + assertEquals(MAX_RETRY_DELAY.hashCode(), maxRetryDelay.hashCode()); + assertEquals(MAX_ATTEMPTS.hashCode(), maxAttempts.hashCode()); + assertEquals(JITTERED.hashCode(), jittered.hashCode()); + } + + @Test + public void testMergeToSettings() { + RetrySettings defRetrySettings = RetrySettings.newBuilder().build(); + + assertEquals(defRetrySettings, RetryOption.mergeToSettings(defRetrySettings)); + + RetrySettings mergedRetrySettings = + RetryOption.mergeToSettings( + defRetrySettings, + TOTAL_TIMEOUT, + INITIAL_RETRY_DELAY, + RETRY_DELAY_MULTIPLIER, + MAX_RETRY_DELAY, + MAX_ATTEMPTS, + JITTERED); + assertEquals(retrySettings, mergedRetrySettings); + + defRetrySettings = + defRetrySettings.toBuilder().setTotalTimeout(Duration.ofMillis(420L)).build(); + mergedRetrySettings = RetryOption.mergeToSettings(defRetrySettings, TOTAL_TIMEOUT); + assertEquals(defRetrySettings, mergedRetrySettings); + + defRetrySettings = + defRetrySettings.toBuilder().setMaxRetryDelay(Duration.ofMillis(100)).build(); + mergedRetrySettings = RetryOption.mergeToSettings(defRetrySettings, MAX_RETRY_DELAY); + assertEquals(defRetrySettings, mergedRetrySettings); + + defRetrySettings = + defRetrySettings.toBuilder().setInitialRetryDelay(Duration.ofMillis(42L)).build(); + mergedRetrySettings = RetryOption.mergeToSettings(defRetrySettings, INITIAL_RETRY_DELAY); + assertEquals(defRetrySettings, mergedRetrySettings); + + defRetrySettings = defRetrySettings.toBuilder().setRetryDelayMultiplier(1.5).build(); + mergedRetrySettings = RetryOption.mergeToSettings(defRetrySettings, RETRY_DELAY_MULTIPLIER); + assertEquals(defRetrySettings, mergedRetrySettings); + + defRetrySettings = defRetrySettings.toBuilder().setMaxAttempts(100).build(); + mergedRetrySettings = RetryOption.mergeToSettings(defRetrySettings, MAX_ATTEMPTS); + assertEquals(defRetrySettings, mergedRetrySettings); + + defRetrySettings = defRetrySettings.toBuilder().setJittered(false).build(); + mergedRetrySettings = RetryOption.mergeToSettings(defRetrySettings, JITTERED); + assertEquals(defRetrySettings, mergedRetrySettings); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/RoleTest.java b/google-cloud-core/src/test/java/com/google/cloud/RoleTest.java new file mode 100644 index 0000000000..d6fcdf8441 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/RoleTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; + +public class RoleTest { + + private static final Role VIEWER = Role.of("viewer"); + private static final Role EDITOR = Role.of("editor"); + private static final Role OWNER = Role.of("owner"); + + @Test + public void testOf() { + assertThat(VIEWER.getValue()).isEqualTo("roles/viewer"); + assertThat(EDITOR.getValue()).isEqualTo("roles/editor"); + assertThat(OWNER.getValue()).isEqualTo("roles/owner"); + compareRoles(VIEWER, Role.of("roles/viewer")); + compareRoles(EDITOR, Role.of("roles/editor")); + compareRoles(OWNER, Role.of("roles/owner")); + + String customRole = "projects/foo/roles/bar"; + assertThat(Role.of(customRole).getValue()).isEqualTo(customRole); + } + + @Test + public void testViewer() { + assertThat(Role.viewer().getValue()).isEqualTo("roles/viewer"); + } + + @Test + public void testEditor() { + assertThat(Role.editor().getValue()).isEqualTo("roles/editor"); + } + + @Test + public void testOwner() { + assertThat(Role.owner().getValue()).isEqualTo("roles/owner"); + } + + @Test(expected = NullPointerException.class) + public void testOfNullValue() { + Role.of(null); + } + + private void compareRoles(Role expected, Role actual) { + assertThat(actual).isEqualTo(expected); + assertThat(actual.getValue()).isEqualTo(expected.getValue()); + assertThat(actual.hashCode()).isEqualTo(expected.hashCode()); + assertThat(actual.toString()).isEqualTo(expected.toString()); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/SerializationTest.java b/google-cloud-core/src/test/java/com/google/cloud/SerializationTest.java new file mode 100644 index 0000000000..6c35c665b5 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/SerializationTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.api.gax.retrying.RetrySettings; +import com.google.cloud.BaseServiceException.ExceptionData; +import com.google.cloud.MonitoredResourceDescriptor.LabelDescriptor; +import com.google.cloud.MonitoredResourceDescriptor.LabelDescriptor.ValueType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.Serializable; +import org.threeten.bp.Duration; + +public class SerializationTest extends BaseSerializationTest { + + private static final BaseServiceException BASE_SERVICE_EXCEPTION = + new BaseServiceException(ExceptionData.from(42, "message", "reason", false)); + private static final ExceptionHandler EXCEPTION_HANDLER = ExceptionHandler.getDefaultInstance(); + private static final Identity IDENTITY = Identity.allAuthenticatedUsers(); + private static final PageImpl PAGE = + new PageImpl<>(null, "cursor", ImmutableList.of("string1", "string2")); + private static final RetrySettings RETRY_SETTINGS = ServiceOptions.getDefaultRetrySettings(); + private static final Role SOME_ROLE = Role.viewer(); + private static final Policy SOME_IAM_POLICY = Policy.newBuilder().build(); + private static final RetryOption CHECKING_PERIOD = + RetryOption.initialRetryDelay(Duration.ofSeconds(42)); + private static final LabelDescriptor LABEL_DESCRIPTOR = + new LabelDescriptor("project_id", ValueType.STRING, "The project id"); + private static final MonitoredResourceDescriptor MONITORED_RESOURCE_DESCRIPTOR = + MonitoredResourceDescriptor.newBuilder("global") + .setLabels(ImmutableList.of(LABEL_DESCRIPTOR)) + .build(); + private static final MonitoredResource MONITORED_RESOURCE = + MonitoredResource.newBuilder("global") + .setLabels(ImmutableMap.of("project_id", "project")) + .build(); + private static final String JSON_KEY = + "{\n" + + " \"private_key_id\": \"somekeyid\",\n" + + " \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggS" + + "kAgEAAoIBAQC+K2hSuFpAdrJI\\nnCgcDz2M7t7bjdlsadsasad+fvRSW6TjNQZ3p5LLQY1kSZRqBqylRkzteMOyHg" + + "aR\\n0Pmxh3ILCND5men43j3h4eDbrhQBuxfEMalkG92sL+PNQSETY2tnvXryOvmBRwa/\\nQP/9dJfIkIDJ9Fw9N4" + + "Bhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nknddadwkwewcVxHFhcZJO+XWf6ofLUXpRwiTZakGMn8EE1uVa2" + + "LgczOjwWHGi99MFjxSer5m9\\n1tCa3/KEGKiS/YL71JvjwX3mb+cewlkcmweBKZHM2JPTk0ZednFSpVZMtycjkbLa" + + "\\ndYOS8V85AgMBewECggEBAKksaldajfDZDV6nGqbFjMiizAKJolr/M3OQw16K6o3/\\n0S31xIe3sSlgW0+UbYlF" + + "4U8KifhManD1apVSC3csafaspP4RZUHFhtBywLO9pR5c\\nr6S5aLp+gPWFyIp1pfXbWGvc5VY/v9x7ya1VEa6rXvL" + + "sKupSeWAW4tMj3eo/64ge\\nsdaceaLYw52KeBYiT6+vpsnYrEkAHO1fF/LavbLLOFJmFTMxmsNaG0tuiJHgjshB\\" + + "n82DpMCbXG9YcCgI/DbzuIjsdj2JC1cascSP//3PmefWysucBQe7Jryb6NQtASmnv\\nCdDw/0jmZTEjpe4S1lxfHp" + + "lAhHFtdgYTvyYtaLZiVVkCgYEA8eVpof2rceecw/I6\\n5ng1q3Hl2usdWV/4mZMvR0fOemacLLfocX6IYxT1zA1FF" + + "JlbXSRsJMf/Qq39mOR2\\nSpW+hr4jCoHeRVYLgsbggtrevGmILAlNoqCMpGZ6vDmJpq6ECV9olliDvpPgWOP+\\nm" + + "YPDreFBGxWvQrADNbRt2dmGsrsCgYEAyUHqB2wvJHFqdmeBsaacewzV8x9WgmeX\\ngUIi9REwXlGDW0Mz50dxpxcK" + + "CAYn65+7TCnY5O/jmL0VRxU1J2mSWyWTo1C+17L0\\n3fUqjxL1pkefwecxwecvC+gFFYdJ4CQ/MHHXU81Lwl1iWdF" + + "Cd2UoGddYaOF+KNeM\\nHC7cmqra+JsCgYEAlUNywzq8nUg7282E+uICfCB0LfwejuymR93CtsFgb7cRd6ak\\nECR" + + "8FGfCpH8ruWJINllbQfcHVCX47ndLZwqv3oVFKh6pAS/vVI4dpOepP8++7y1u\\ncoOvtreXCX6XqfrWDtKIvv0vjl" + + "HBhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nkndj5uNl5SiuVxHFhcZJO+XWf6ofLUregtevZakGMn8EE1uVa" + + "2AY7eafmoU/nZPT\\n00YB0TBATdCbn/nBSuKDESkhSg9s2GEKQZG5hBmL5uCMfo09z3SfxZIhJdlerreP\\nJ7gSi" + + "dI12N+EZxYd4xIJh/HFDgp7RRO87f+WJkofMQKBgGTnClK1VMaCRbJZPriw\\nEfeFCoOX75MxKwXs6xgrw4W//AYG" + + "GUjDt83lD6AZP6tws7gJ2IwY/qP7+lyhjEqN\\nHtfPZRGFkGZsdaksdlaksd323423d+15/UvrlRSFPNj1tWQmNKk" + + "XyRDW4IG1Oa2p\\nrALStNBx5Y9t0/LQnFI4w3aG\\n-----END PRIVATE KEY-----\\n\",\n" + + " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n" + + " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n" + + " \"type\": \"service_account\"\n" + + "}"; + + @Override + protected Serializable[] serializableObjects() { + return new Serializable[] { + BASE_SERVICE_EXCEPTION, + EXCEPTION_HANDLER, + IDENTITY, + PAGE, + RETRY_SETTINGS, + SOME_ROLE, + SOME_IAM_POLICY, + CHECKING_PERIOD, + LABEL_DESCRIPTOR, + MONITORED_RESOURCE_DESCRIPTOR, + MONITORED_RESOURCE + }; + } + + @Override + protected Restorable[] restorableObjects() { + return null; + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java b/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java new file mode 100644 index 0000000000..3ce9ffd543 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java @@ -0,0 +1,431 @@ +/* + * Copyright 2015 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.assertFalse; +import static org.junit.Assert.assertEquals; +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.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.HttpTesting; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.api.core.ApiClock; +import com.google.api.core.CurrentMillisClock; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.spi.ServiceRpcFactory; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.io.Files; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class ServiceOptionsTest { + private static final String JSON_KEY = + "{\n" + + " \"private_key_id\": \"somekeyid\",\n" + + " \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggS" + + "kAgEAAoIBAQC+K2hSuFpAdrJI\\nnCgcDz2M7t7bjdlsadsasad+fvRSW6TjNQZ3p5LLQY1kSZRqBqylRkzteMOyHg" + + "aR\\n0Pmxh3ILCND5men43j3h4eDbrhQBuxfEMalkG92sL+PNQSETY2tnvXryOvmBRwa/\\nQP/9dJfIkIDJ9Fw9N4" + + "Bhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nknddadwkwewcVxHFhcZJO+XWf6ofLUXpRwiTZakGMn8EE1uVa2" + + "LgczOjwWHGi99MFjxSer5m9\\n1tCa3/KEGKiS/YL71JvjwX3mb+cewlkcmweBKZHM2JPTk0ZednFSpVZMtycjkbLa" + + "\\ndYOS8V85AgMBewECggEBAKksaldajfDZDV6nGqbFjMiizAKJolr/M3OQw16K6o3/\\n0S31xIe3sSlgW0+UbYlF" + + "4U8KifhManD1apVSC3csafaspP4RZUHFhtBywLO9pR5c\\nr6S5aLp+gPWFyIp1pfXbWGvc5VY/v9x7ya1VEa6rXvL" + + "sKupSeWAW4tMj3eo/64ge\\nsdaceaLYw52KeBYiT6+vpsnYrEkAHO1fF/LavbLLOFJmFTMxmsNaG0tuiJHgjshB\\" + + "n82DpMCbXG9YcCgI/DbzuIjsdj2JC1cascSP//3PmefWysucBQe7Jryb6NQtASmnv\\nCdDw/0jmZTEjpe4S1lxfHp" + + "lAhHFtdgYTvyYtaLZiVVkCgYEA8eVpof2rceecw/I6\\n5ng1q3Hl2usdWV/4mZMvR0fOemacLLfocX6IYxT1zA1FF" + + "JlbXSRsJMf/Qq39mOR2\\nSpW+hr4jCoHeRVYLgsbggtrevGmILAlNoqCMpGZ6vDmJpq6ECV9olliDvpPgWOP+\\nm" + + "YPDreFBGxWvQrADNbRt2dmGsrsCgYEAyUHqB2wvJHFqdmeBsaacewzV8x9WgmeX\\ngUIi9REwXlGDW0Mz50dxpxcK" + + "CAYn65+7TCnY5O/jmL0VRxU1J2mSWyWTo1C+17L0\\n3fUqjxL1pkefwecxwecvC+gFFYdJ4CQ/MHHXU81Lwl1iWdF" + + "Cd2UoGddYaOF+KNeM\\nHC7cmqra+JsCgYEAlUNywzq8nUg7282E+uICfCB0LfwejuymR93CtsFgb7cRd6ak\\nECR" + + "8FGfCpH8ruWJINllbQfcHVCX47ndLZwqv3oVFKh6pAS/vVI4dpOepP8++7y1u\\ncoOvtreXCX6XqfrWDtKIvv0vjl" + + "HBhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nkndj5uNl5SiuVxHFhcZJO+XWf6ofLUregtevZakGMn8EE1uVa" + + "2AY7eafmoU/nZPT\\n00YB0TBATdCbn/nBSuKDESkhSg9s2GEKQZG5hBmL5uCMfo09z3SfxZIhJdlerreP\\nJ7gSi" + + "dI12N+EZxYd4xIJh/HFDgp7RRO87f+WJkofMQKBgGTnClK1VMaCRbJZPriw\\nEfeFCoOX75MxKwXs6xgrw4W//AYG" + + "GUjDt83lD6AZP6tws7gJ2IwY/qP7+lyhjEqN\\nHtfPZRGFkGZsdaksdlaksd323423d+15/UvrlRSFPNj1tWQmNKk" + + "XyRDW4IG1Oa2p\\nrALStNBx5Y9t0/LQnFI4w3aG\\n-----END PRIVATE KEY-----\\n\",\n" + + " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n" + + " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n" + + " \"type\": \"service_account\"\n" + + "}"; + private static GoogleCredentials credentials; + + static { + try { + InputStream keyStream = new ByteArrayInputStream(JSON_KEY.getBytes()); + credentials = GoogleCredentials.fromStream(keyStream); + } catch (IOException e) { + fail("Couldn't create fake JSON credentials."); + } + } + + private static final String JSON_KEY_PROJECT_ID = + "{\n" + + " \"private_key_id\": \"somekeyid\",\n" + + " \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggS" + + "kAgEAAoIBAQC+K2hSuFpAdrJI\\nnCgcDz2M7t7bjdlsadsasad+fvRSW6TjNQZ3p5LLQY1kSZRqBqylRkzteMOyHg" + + "aR\\n0Pmxh3ILCND5men43j3h4eDbrhQBuxfEMalkG92sL+PNQSETY2tnvXryOvmBRwa/\\nQP/9dJfIkIDJ9Fw9N4" + + "Bhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nknddadwkwewcVxHFhcZJO+XWf6ofLUXpRwiTZakGMn8EE1uVa2" + + "LgczOjwWHGi99MFjxSer5m9\\n1tCa3/KEGKiS/YL71JvjwX3mb+cewlkcmweBKZHM2JPTk0ZednFSpVZMtycjkbLa" + + "\\ndYOS8V85AgMBewECggEBAKksaldajfDZDV6nGqbFjMiizAKJolr/M3OQw16K6o3/\\n0S31xIe3sSlgW0+UbYlF" + + "4U8KifhManD1apVSC3csafaspP4RZUHFhtBywLO9pR5c\\nr6S5aLp+gPWFyIp1pfXbWGvc5VY/v9x7ya1VEa6rXvL" + + "sKupSeWAW4tMj3eo/64ge\\nsdaceaLYw52KeBYiT6+vpsnYrEkAHO1fF/LavbLLOFJmFTMxmsNaG0tuiJHgjshB\\" + + "n82DpMCbXG9YcCgI/DbzuIjsdj2JC1cascSP//3PmefWysucBQe7Jryb6NQtASmnv\\nCdDw/0jmZTEjpe4S1lxfHp" + + "lAhHFtdgYTvyYtaLZiVVkCgYEA8eVpof2rceecw/I6\\n5ng1q3Hl2usdWV/4mZMvR0fOemacLLfocX6IYxT1zA1FF" + + "JlbXSRsJMf/Qq39mOR2\\nSpW+hr4jCoHeRVYLgsbggtrevGmILAlNoqCMpGZ6vDmJpq6ECV9olliDvpPgWOP+\\nm" + + "YPDreFBGxWvQrADNbRt2dmGsrsCgYEAyUHqB2wvJHFqdmeBsaacewzV8x9WgmeX\\ngUIi9REwXlGDW0Mz50dxpxcK" + + "CAYn65+7TCnY5O/jmL0VRxU1J2mSWyWTo1C+17L0\\n3fUqjxL1pkefwecxwecvC+gFFYdJ4CQ/MHHXU81Lwl1iWdF" + + "Cd2UoGddYaOF+KNeM\\nHC7cmqra+JsCgYEAlUNywzq8nUg7282E+uICfCB0LfwejuymR93CtsFgb7cRd6ak\\nECR" + + "8FGfCpH8ruWJINllbQfcHVCX47ndLZwqv3oVFKh6pAS/vVI4dpOepP8++7y1u\\ncoOvtreXCX6XqfrWDtKIvv0vjl" + + "HBhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nkndj5uNl5SiuVxHFhcZJO+XWf6ofLUregtevZakGMn8EE1uVa" + + "2AY7eafmoU/nZPT\\n00YB0TBATdCbn/nBSuKDESkhSg9s2GEKQZG5hBmL5uCMfo09z3SfxZIhJdlerreP\\nJ7gSi" + + "dI12N+EZxYd4xIJh/HFDgp7RRO87f+WJkofMQKBgGTnClK1VMaCRbJZPriw\\nEfeFCoOX75MxKwXs6xgrw4W//AYG" + + "GUjDt83lD6AZP6tws7gJ2IwY/qP7+lyhjEqN\\nHtfPZRGFkGZsdaksdlaksd323423d+15/UvrlRSFPNj1tWQmNKk" + + "XyRDW4IG1Oa2p\\nrALStNBx5Y9t0/LQnFI4w3aG\\n-----END PRIVATE KEY-----\\n\",\n" + + " \"project_id\": \"someprojectid\",\n" + + " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n" + + " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n" + + " \"type\": \"service_account\"\n" + + "}"; + private static GoogleCredentials credentialsWithProjectId; + + static { + try { + InputStream keyStream = new ByteArrayInputStream(JSON_KEY_PROJECT_ID.getBytes()); + credentialsWithProjectId = GoogleCredentials.fromStream(keyStream); + } catch (IOException e) { + fail("Couldn't create fake JSON credentials."); + } + } + + private static final ApiClock TEST_CLOCK = new TestClock(); + private static final TestServiceOptions OPTIONS = + TestServiceOptions.newBuilder() + .setCredentials(credentials) + .setClock(TEST_CLOCK) + .setHost("host") + .setProjectId("project-id") + .setRetrySettings(ServiceOptions.getNoRetrySettings()) + .build(); + private static final TestServiceOptions OPTIONS_NO_CREDENTIALS = + TestServiceOptions.newBuilder() + .setCredentials(NoCredentials.getInstance()) + .setClock(TEST_CLOCK) + .setHost("host") + .setProjectId("project-id") + .setRetrySettings(ServiceOptions.getNoRetrySettings()) + .build(); + private static final TestServiceOptions DEFAULT_OPTIONS = + TestServiceOptions.newBuilder().setProjectId("project-id").build(); + private static final TestServiceOptions OPTIONS_COPY = OPTIONS.toBuilder().build(); + private static final String LIBRARY_NAME = "gcloud-java"; + private static final Pattern APPLICATION_NAME_PATTERN = Pattern.compile(LIBRARY_NAME + "/.*"); + + @Rule public ExpectedException thrown = ExpectedException.none(); + + private static class TestClock implements ApiClock { + @Override + public long nanoTime() { + return 123_456_789_000_000L; + } + + @Override + public long millisTime() { + return 123_456_789L; + } + } + + interface TestService extends Service {} + + private static class TestServiceImpl extends BaseService + implements TestService { + private TestServiceImpl(TestServiceOptions options) { + super(options); + } + } + + public interface TestServiceFactory extends ServiceFactory {} + + private static class DefaultTestServiceFactory implements TestServiceFactory { + private static final TestServiceFactory INSTANCE = new DefaultTestServiceFactory(); + + @Override + public TestService create(TestServiceOptions options) { + return new TestServiceImpl(options); + } + } + + public interface TestServiceRpcFactory extends ServiceRpcFactory {} + + private static class DefaultTestServiceRpcFactory implements TestServiceRpcFactory { + private static final TestServiceRpcFactory INSTANCE = new DefaultTestServiceRpcFactory(); + + @Override + public TestServiceRpc create(TestServiceOptions options) { + return new DefaultTestServiceRpc(options); + } + } + + private interface TestServiceRpc extends ServiceRpc {} + + private static class DefaultTestServiceRpc implements TestServiceRpc { + DefaultTestServiceRpc(TestServiceOptions options) {} + } + + static class TestServiceOptions extends ServiceOptions { + private static class Builder + extends ServiceOptions.Builder { + private Builder() {} + + private Builder(TestServiceOptions options) { + super(options); + } + + @Override + protected TestServiceOptions build() { + return new TestServiceOptions(this); + } + } + + private TestServiceOptions(Builder builder) { + super( + TestServiceFactory.class, + TestServiceRpcFactory.class, + builder, + new TestServiceDefaults()); + } + + private static class TestServiceDefaults + implements ServiceDefaults { + + @Override + public TestServiceFactory getDefaultServiceFactory() { + return DefaultTestServiceFactory.INSTANCE; + } + + @Override + public TestServiceRpcFactory getDefaultRpcFactory() { + return DefaultTestServiceRpcFactory.INSTANCE; + } + + @Override + public TransportOptions getDefaultTransportOptions() { + return new TransportOptions() {}; + } + } + + @Override + protected Set getScopes() { + return null; + } + + @Override + public Builder toBuilder() { + return new Builder(this); + } + + private static Builder newBuilder() { + return new Builder(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof TestServiceOptions && baseEquals((TestServiceOptions) obj); + } + + @Override + public int hashCode() { + return baseHashCode(); + } + } + + @Test + public void testBuilder() { + assertSame(credentials, OPTIONS.getCredentials()); + assertSame(TEST_CLOCK, OPTIONS.getClock()); + assertEquals("host", OPTIONS.getHost()); + assertEquals("project-id", OPTIONS.getProjectId()); + assertSame(ServiceOptions.getNoRetrySettings(), OPTIONS.getRetrySettings()); + assertSame(CurrentMillisClock.getDefaultClock(), DEFAULT_OPTIONS.getClock()); + assertEquals("https://www.googleapis.com", DEFAULT_OPTIONS.getHost()); + assertSame(ServiceOptions.getDefaultRetrySettings(), DEFAULT_OPTIONS.getRetrySettings()); + } + + @Test + public void testBuilderNoCredentials() { + assertEquals(NoCredentials.getInstance(), OPTIONS_NO_CREDENTIALS.getCredentials()); + assertTrue(NoCredentials.getInstance().equals(OPTIONS_NO_CREDENTIALS.getCredentials())); + assertFalse(NoCredentials.getInstance().equals(OPTIONS.getCredentials())); + assertFalse(NoCredentials.getInstance().equals(null)); + assertSame(TEST_CLOCK, OPTIONS_NO_CREDENTIALS.getClock()); + assertEquals("host", OPTIONS_NO_CREDENTIALS.getHost()); + assertEquals("project-id", OPTIONS_NO_CREDENTIALS.getProjectId()); + assertSame(ServiceOptions.getNoRetrySettings(), OPTIONS_NO_CREDENTIALS.getRetrySettings()); + } + + @Test + public void testBuilderNullCredentials() { + thrown.expect(NullPointerException.class); + TestServiceOptions.newBuilder().setCredentials(null).build(); + } + + @Test + public void testBuilderServiceAccount_setsProjectId() { + TestServiceOptions options = + TestServiceOptions.newBuilder().setCredentials(credentialsWithProjectId).build(); + assertEquals("someprojectid", options.getProjectId()); + } + + @Test + public void testBuilderServiceAccount_explicitSetProjectIdBefore() { + TestServiceOptions options = + TestServiceOptions.newBuilder() + .setProjectId("override-project-id") + .setCredentials(credentialsWithProjectId) + .build(); + assertEquals("override-project-id", options.getProjectId()); + } + + @Test + public void testBuilderServiceAccount_explicitSetProjectIdAfter() { + TestServiceOptions options = + TestServiceOptions.newBuilder() + .setCredentials(credentialsWithProjectId) + .setProjectId("override-project-id") + .build(); + assertEquals("override-project-id", options.getProjectId()); + } + + @Test + public void testGetProjectIdRequired() { + assertTrue(OPTIONS.projectIdRequired()); + } + + @Test + public void testService() { + assertTrue(OPTIONS.getService() instanceof TestServiceImpl); + } + + @Test + public void testRpc() { + assertTrue(OPTIONS.getRpc() instanceof DefaultTestServiceRpc); + } + + @Test + public void testBaseEquals() { + assertEquals(OPTIONS, OPTIONS_COPY); + assertNotEquals(DEFAULT_OPTIONS, OPTIONS); + } + + @Test + public void testLibraryName() { + assertEquals(LIBRARY_NAME, ServiceOptions.getLibraryName()); + } + + @Test + public void testApplicationName() { + assertTrue(APPLICATION_NAME_PATTERN.matcher(OPTIONS.getApplicationName()).matches()); + } + + @Test + public void testBaseHashCode() { + assertEquals(OPTIONS.hashCode(), OPTIONS_COPY.hashCode()); + assertNotEquals(DEFAULT_OPTIONS.hashCode(), OPTIONS.hashCode()); + } + + @Test + public void testGetServiceAccountProjectId() throws Exception { + File credentialsFile = File.createTempFile("credentials", ".json"); + credentialsFile.deleteOnExit(); + Files.write("{\"project_id\":\"my-project-id\"}".getBytes(), credentialsFile); + + assertEquals( + "my-project-id", ServiceOptions.getServiceAccountProjectId(credentialsFile.getPath())); + } + + @Test + public void testGetServiceAccountProjectId_badJson() throws Exception { + File credentialsFile = File.createTempFile("credentials", ".json"); + credentialsFile.deleteOnExit(); + Files.write("asdfghj".getBytes(), credentialsFile); + + assertNull(ServiceOptions.getServiceAccountProjectId(credentialsFile.getPath())); + } + + @Test + public void testGetServiceAccountProjectId_nonExistentFile() throws Exception { + File credentialsFile = new File("/doesnotexist"); + + assertNull(ServiceOptions.getServiceAccountProjectId(credentialsFile.getPath())); + } + + @Test + public void testResponseHeaderContainsMetaDataFlavor() throws Exception { + Multimap headers = ArrayListMultimap.create(); + headers.put("Metadata-Flavor", "Google"); + HttpResponse httpResponse = createHttpResponseWithHeader(headers); + assertThat(ServiceOptions.headerContainsMetadataFlavor(httpResponse)).isTrue(); + } + + @Test + public void testResponseHeaderDoesNotContainMetaDataFlavor() throws Exception { + Multimap headers = ArrayListMultimap.create(); + HttpResponse httpResponse = createHttpResponseWithHeader(headers); + assertThat(ServiceOptions.headerContainsMetadataFlavor(httpResponse)).isFalse(); + } + + private HttpResponse createHttpResponseWithHeader(final Multimap headers) + throws Exception { + HttpTransport mockHttpTransport = + new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + for (Map.Entry entry : headers.entries()) { + response.addHeader(entry.getKey(), entry.getValue()); + } + return response; + } + }; + } + }; + HttpRequest request = + mockHttpTransport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL); + return request.execute(); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/StringEnumTest.java b/google-cloud-core/src/test/java/com/google/cloud/StringEnumTest.java new file mode 100644 index 0000000000..716e124f53 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/StringEnumTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.core.ApiFunction; +import com.google.common.testing.EqualsTester; +import java.util.Arrays; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class StringEnumTest { + + @Rule public ExpectedException expectedException = ExpectedException.none(); + + public static class Letter extends StringEnumValue { + private static final long serialVersionUID = -1717976087182628526L; + + private Letter(String constant) { + super(constant); + } + + private static final ApiFunction CONSTRUCTOR = + new ApiFunction() { + @Override + public Letter apply(String constant) { + return new Letter(constant); + } + }; + + private static final StringEnumType type = + new StringEnumType(Letter.class, CONSTRUCTOR); + + public static final Letter A = type.createAndRegister("A"); + public static final Letter B = type.createAndRegister("B"); + public static final Letter C = type.createAndRegister("C"); + + public static Letter valueOfStrict(String constant) { + return type.valueOfStrict(constant); + } + + /** Get the StorageClass for the given String constant, and allow unrecognized values. */ + public static Letter valueOf(String constant) { + return type.valueOf(constant); + } + + /** Return the known values for StorageClass. */ + public static Letter[] values() { + return type.values(); + } + } + + @Test + public void testNullClass() { + expectedException.expect(NullPointerException.class); + new StringEnumType(null, Letter.CONSTRUCTOR); + } + + @Test + public void testNullConstructor() { + expectedException.expect(NullPointerException.class); + new StringEnumType(Letter.class, null); + } + + @Test + public void testEnumInstances() { + assertThat(Letter.A.toString()).isEqualTo("A"); + } + + @Test + public void testValueOf() { + assertThat(Letter.valueOf("A")).isSameInstanceAs(Letter.A); + assertThat(Letter.valueOf("B")).isSameInstanceAs(Letter.B); + assertThat(Letter.valueOf("C")).isSameInstanceAs(Letter.C); + assertThat(Letter.valueOf("NonExistentLetter").toString()).isEqualTo("NonExistentLetter"); + } + + @Test + public void testValueOfStrict() { + assertThat(Letter.valueOfStrict("A")).isSameInstanceAs(Letter.A); + assertThat(Letter.valueOfStrict("B")).isSameInstanceAs(Letter.B); + assertThat(Letter.valueOfStrict("C")).isSameInstanceAs(Letter.C); + } + + @Test + public void testEquals() { + EqualsTester tester = new EqualsTester(); + + tester.addEqualityGroup(Letter.A, Letter.valueOf("A"), Letter.valueOfStrict("A")); + tester.addEqualityGroup(Letter.B, Letter.valueOf("B"), Letter.valueOfStrict("B")); + tester.addEqualityGroup(Letter.C, Letter.valueOf("C"), Letter.valueOfStrict("C")); + tester.addEqualityGroup( + Letter.valueOf("NonExistentLetter"), Letter.valueOf("NonExistentLetter")); + } + + @Test + public void testValueOfStrict_invalid() { + expectedException.expect(IllegalArgumentException.class); + Letter.valueOfStrict("NonExistentLetter"); + } + + @Test + public void testValues() { + assertThat( + Arrays.asList(Letter.values()).containsAll(Arrays.asList(Letter.A, Letter.B, Letter.C))); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/TimestampTest.java b/google-cloud-core/src/test/java/com/google/cloud/TimestampTest.java new file mode 100644 index 0000000000..db9ede84dc --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/TimestampTest.java @@ -0,0 +1,231 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.testing.SerializableTester.reserializeAndAssert; +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.testing.EqualsTester; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link com.google.cloud.Timestamp}. */ +@RunWith(JUnit4.class) +public class TimestampTest { + private static final String TEST_TIME_ISO = "2015-10-12T15:14:54Z"; + private static final long TEST_TIME_SECONDS = 1444662894L; + private static final long TEST_TIME_MICROSECONDS = 10000100L; + private static final long TEST_TIME_MILLISECONDS = + TimeUnit.SECONDS.toMillis(1444662894L) + TimeUnit.MICROSECONDS.toMillis(1234); + private static final long TEST_TIME_MILLISECONDS_NEGATIVE = -1000L; + private static final Date TEST_DATE = new Date(TEST_TIME_MILLISECONDS); + private static final Date TEST_DATE_PRE_EPOCH = new Date(TEST_TIME_MILLISECONDS_NEGATIVE); + + @Rule public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void minValue() { + // MIN_VALUE is before the start of the Gregorian calendar... use magic value. + assertThat(Timestamp.MIN_VALUE.getSeconds()).isEqualTo(-62135596800L); + assertThat(Timestamp.MIN_VALUE.getNanos()).isEqualTo(0); + } + + @Test + public void maxValue() { + TimeZone tz = TimeZone.getTimeZone("UTC"); + GregorianCalendar calendar = new GregorianCalendar(tz); + calendar.set(9999, Calendar.DECEMBER, 31, 23, 59, 59); + java.sql.Timestamp expectedMin = new java.sql.Timestamp(calendar.getTimeInMillis()); + expectedMin.setNanos(999999999); + + assertThat(Timestamp.MAX_VALUE.getSeconds()).isEqualTo(calendar.getTimeInMillis() / 1000L); + assertThat(Timestamp.MAX_VALUE.getNanos()).isEqualTo(999999999); + } + + @Test + public void ofMicroseconds() { + Timestamp timestamp = Timestamp.ofTimeMicroseconds(TEST_TIME_MICROSECONDS); + assertThat(timestamp.getSeconds()).isEqualTo(TEST_TIME_MICROSECONDS / 1000000L); + assertThat(timestamp.getNanos()).isEqualTo(TEST_TIME_MICROSECONDS % 1000000L * 1000); + } + + @Test + public void ofDate() { + Timestamp timestamp = Timestamp.of(TEST_DATE); + Long expectedSeconds = TimeUnit.MILLISECONDS.toSeconds(TEST_TIME_MILLISECONDS); + Long expectedNanos = + TimeUnit.MILLISECONDS.toNanos(TEST_TIME_MILLISECONDS) + - TimeUnit.SECONDS.toNanos(expectedSeconds); + assertThat(timestamp.getSeconds()).isEqualTo(expectedSeconds); + assertThat(timestamp.getNanos()).isEqualTo(expectedNanos); + } + + @Test + public void ofDatePreEpoch() { + Timestamp timestamp = Timestamp.of(TEST_DATE_PRE_EPOCH); + long expectedSeconds = TEST_TIME_MILLISECONDS_NEGATIVE / 1_000; + int expectedNanos = (int) (TEST_TIME_MILLISECONDS_NEGATIVE % 1_000 * 1000_000); + if (expectedNanos < 0) { + expectedSeconds--; + expectedNanos += 1_000_000_000; + } + assertThat(timestamp.getSeconds()).isEqualTo(expectedSeconds); + assertThat(timestamp.getNanos()).isEqualTo(expectedNanos); + } + + @Test + public void toDate() { + Timestamp timestamp = Timestamp.ofTimeSecondsAndNanos(TEST_TIME_SECONDS, 1234 * 1000); + Date date = timestamp.toDate(); + assertThat(TEST_TIME_MILLISECONDS).isEqualTo(date.getTime()); + } + + @Test + public void toFromSqlTimestamp() { + long seconds = TEST_TIME_SECONDS; + int nanos = 500000000; + + java.sql.Timestamp sqlTs = new java.sql.Timestamp(seconds * 1000); + sqlTs.setNanos(nanos); + + Timestamp ts = Timestamp.of(sqlTs); + assertThat(ts.getSeconds()).isEqualTo(seconds); + assertThat(ts.getNanos()).isEqualTo(nanos); + + assertThat(ts.toSqlTimestamp()).isEqualTo(sqlTs); + } + + @Test + public void boundsSecondsMin() { + expectedException.expect(IllegalArgumentException.class); + Timestamp.ofTimeSecondsAndNanos(Timestamp.MIN_VALUE.getSeconds() - 1, 999999999); + } + + @Test + public void boundsSecondsMax() { + expectedException.expect(IllegalArgumentException.class); + Timestamp.ofTimeSecondsAndNanos(Timestamp.MAX_VALUE.getSeconds() + 1, 0); + } + + @Test + public void boundsNanosMin() { + expectedException.expect(IllegalArgumentException.class); + Timestamp.ofTimeSecondsAndNanos(TEST_TIME_SECONDS, -1); + } + + @Test + public void boundsNanosMax() { + expectedException.expect(IllegalArgumentException.class); + Timestamp.ofTimeSecondsAndNanos(TEST_TIME_SECONDS, 1000000000); + } + + @Test + public void boundsSqlTimestampMin() { + expectedException.expect(IllegalArgumentException.class); + Timestamp.of(new java.sql.Timestamp((Timestamp.MIN_VALUE.getSeconds() - 1) * 1000)); + } + + @Test + public void boundsSqlTimestampMax() { + expectedException.expect(IllegalArgumentException.class); + Timestamp.of(new java.sql.Timestamp((Timestamp.MAX_VALUE.getSeconds() + 1) * 1000)); + } + + @Test + public void equalsAndHashCode() { + EqualsTester tester = new EqualsTester(); + tester.addEqualityGroup( + Timestamp.ofTimeSecondsAndNanos(TEST_TIME_SECONDS, 0), + Timestamp.ofTimeSecondsAndNanos(TEST_TIME_SECONDS, 0), + Timestamp.of(new java.sql.Timestamp(TEST_TIME_SECONDS * 1000))); + tester.addEqualityGroup(Timestamp.ofTimeSecondsAndNanos(TEST_TIME_SECONDS + 1, 0)); + tester.addEqualityGroup(Timestamp.ofTimeSecondsAndNanos(TEST_TIME_SECONDS, 1)); + tester.testEquals(); + } + + @Test + public void testToString() { + assertThat(Timestamp.MIN_VALUE.toString()).isEqualTo("0001-01-01T00:00:00Z"); + assertThat(Timestamp.MAX_VALUE.toString()).isEqualTo("9999-12-31T23:59:59.999999999Z"); + assertThat(Timestamp.ofTimeSecondsAndNanos(0, 0).toString()).isEqualTo("1970-01-01T00:00:00Z"); + assertThat(Timestamp.ofTimeSecondsAndNanos(0, 100).toString()) + .isEqualTo("1970-01-01T00:00:00.000000100Z"); + assertThat(Timestamp.ofTimeSecondsAndNanos(TEST_TIME_SECONDS, 0).toString()) + .isEqualTo(TEST_TIME_ISO); + } + + @Test + public void parseTimestamp() { + assertThat(Timestamp.parseTimestamp("0001-01-01T00:00:00Z")).isEqualTo(Timestamp.MIN_VALUE); + assertThat(Timestamp.parseTimestamp("9999-12-31T23:59:59.999999999Z")) + .isEqualTo(Timestamp.MAX_VALUE); + assertThat(Timestamp.parseTimestamp(TEST_TIME_ISO)) + .isEqualTo(Timestamp.ofTimeSecondsAndNanos(TEST_TIME_SECONDS, 0)); + } + + @Test + public void parseTimestampWithoutTimeZoneOffset() { + assertThat(Timestamp.parseTimestamp("0001-01-01T00:00:00")).isEqualTo(Timestamp.MIN_VALUE); + assertThat(Timestamp.parseTimestamp("9999-12-31T23:59:59.999999999")) + .isEqualTo(Timestamp.MAX_VALUE); + assertThat(Timestamp.parseTimestamp("2015-10-12T15:14:54")) + .isEqualTo(Timestamp.ofTimeSecondsAndNanos(TEST_TIME_SECONDS, 0)); + } + + @Test + public void fromProto() { + com.google.protobuf.Timestamp proto = + com.google.protobuf.Timestamp.newBuilder().setSeconds(1234).setNanos(567).build(); + Timestamp timestamp = Timestamp.fromProto(proto); + assertThat(timestamp.getSeconds()).isEqualTo(1234); + assertThat(timestamp.getNanos()).isEqualTo(567); + } + + @Test + public void comparable() { + assertThat(Timestamp.MIN_VALUE).isLessThan(Timestamp.MAX_VALUE); + assertThat(Timestamp.MAX_VALUE).isGreaterThan(Timestamp.MIN_VALUE); + + assertThat(Timestamp.ofTimeSecondsAndNanos(100, 0)) + .isAtLeast(Timestamp.ofTimeSecondsAndNanos(100, 0)); + assertThat(Timestamp.ofTimeSecondsAndNanos(100, 0)) + .isAtMost(Timestamp.ofTimeSecondsAndNanos(100, 0)); + + assertThat(Timestamp.ofTimeSecondsAndNanos(100, 1000)) + .isLessThan(Timestamp.ofTimeSecondsAndNanos(101, 0)); + assertThat(Timestamp.ofTimeSecondsAndNanos(100, 1000)) + .isAtMost(Timestamp.ofTimeSecondsAndNanos(101, 0)); + + assertThat(Timestamp.ofTimeSecondsAndNanos(101, 0)) + .isGreaterThan(Timestamp.ofTimeSecondsAndNanos(100, 1000)); + assertThat(Timestamp.ofTimeSecondsAndNanos(101, 0)) + .isAtLeast(Timestamp.ofTimeSecondsAndNanos(100, 1000)); + } + + @Test + public void serialization() throws Exception { + reserializeAndAssert(Timestamp.parseTimestamp("9999-12-31T23:59:59.999999999Z")); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/testing/BaseEmulatorHelperTest.java b/google-cloud-core/src/test/java/com/google/cloud/testing/BaseEmulatorHelperTest.java new file mode 100644 index 0000000000..3a799e2044 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/testing/BaseEmulatorHelperTest.java @@ -0,0 +1,183 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.testing; + +import com.google.api.client.util.Charsets; +import com.google.cloud.ServiceOptions; +import com.google.cloud.testing.BaseEmulatorHelper.EmulatorRunner; +import com.google.common.collect.ImmutableList; +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; +import org.easymock.EasyMock; +import org.junit.Test; +import org.threeten.bp.Duration; + +public class BaseEmulatorHelperTest { + + private static final String BLOCK_UNTIL = "Block until"; + + private static class TestEmulatorHelper extends BaseEmulatorHelper { + + private final List runners; + private final String blockUntil; + + private TestEmulatorHelper(List runners, String blockUntil) { + super("emulator", 1, "project"); + this.runners = runners; + this.blockUntil = blockUntil; + } + + @Override + protected List getEmulatorRunners() { + return runners; + } + + @Override + protected Logger getLogger() { + return null; + } + + @Override + public ServiceOptions getOptions() { + return null; + } + + @Override + public void start() throws IOException, InterruptedException { + startProcess(blockUntil); + } + + @Override + public void stop(Duration timeout) throws IOException, InterruptedException, TimeoutException { + waitForProcess(timeout); + } + + @Override + public void reset() throws IOException { + // do nothing + } + } + + @Test + public void testEmulatorHelper() throws IOException, InterruptedException, TimeoutException { + Process process = EasyMock.createStrictMock(Process.class); + InputStream stream = new ByteArrayInputStream(BLOCK_UNTIL.getBytes(Charsets.UTF_8)); + EmulatorRunner emulatorRunner = EasyMock.createStrictMock(EmulatorRunner.class); + EasyMock.expect(process.getInputStream()).andReturn(stream); + EasyMock.expect(emulatorRunner.isAvailable()).andReturn(true); + emulatorRunner.start(); + EasyMock.expectLastCall(); + EasyMock.expect(emulatorRunner.getProcess()).andReturn(process); + emulatorRunner.waitFor(Duration.ofMinutes(1)); + EasyMock.expectLastCall().andReturn(0); + EasyMock.replay(process, emulatorRunner); + TestEmulatorHelper helper = + new TestEmulatorHelper(ImmutableList.of(emulatorRunner), BLOCK_UNTIL); + helper.start(); + helper.stop(Duration.ofMinutes(1)); + EasyMock.verify(); + } + + @Test + public void testEmulatorHelperDownloadWithRetries() + throws IOException, InterruptedException, TimeoutException { + String mockExternalForm = "mockExternalForm"; + String mockInputStream = "mockInputStream"; + String mockProtocol = "mockProtocol"; + String mockFile = "mockFile"; + String mockCommandText = "mockCommandText"; + + MockURLStreamHandler mockURLStreamHandler = EasyMock.createMock(MockURLStreamHandler.class); + URLConnection mockURLConnection = EasyMock.mock(URLConnection.class); + + EasyMock.expect(mockURLStreamHandler.toExternalForm(EasyMock.anyObject(URL.class))) + .andReturn(mockExternalForm) + .anyTimes(); + EasyMock.expect(mockURLConnection.getInputStream()) + .andReturn(new ByteArrayInputStream(mockInputStream.getBytes())) + .anyTimes(); + EasyMock.expect(mockURLStreamHandler.openConnection(EasyMock.anyObject(URL.class))) + .andThrow(new EOFException()) + .times(1); + EasyMock.expect(mockURLStreamHandler.openConnection(EasyMock.anyObject(URL.class))) + .andReturn(mockURLConnection) + .times(1); + EasyMock.replay(mockURLStreamHandler, mockURLConnection); + + URL url = new URL(mockProtocol, null, 0, mockFile, mockURLStreamHandler); + BaseEmulatorHelper.DownloadableEmulatorRunner runner = + new BaseEmulatorHelper.DownloadableEmulatorRunner( + ImmutableList.of(mockCommandText), url, null); + + File cachedFile = new File(System.getProperty("java.io.tmpdir"), mockExternalForm); + cachedFile.delete(); // Clear the cached version so we're always testing the download + + runner.start(); + + EasyMock.verify(); + + cachedFile.delete(); // Cleanup + } + + @Test + public void testEmulatorHelperMultipleRunners() + throws IOException, InterruptedException, TimeoutException { + Process process = EasyMock.createStrictMock(Process.class); + InputStream stream = new ByteArrayInputStream(BLOCK_UNTIL.getBytes(Charsets.UTF_8)); + EmulatorRunner firstRunner = EasyMock.createStrictMock(EmulatorRunner.class); + EmulatorRunner secondRunner = EasyMock.createStrictMock(EmulatorRunner.class); + EasyMock.expect(process.getInputStream()).andReturn(stream); + EasyMock.expect(firstRunner.isAvailable()).andReturn(false); + EasyMock.expect(secondRunner.isAvailable()).andReturn(true); + secondRunner.start(); + EasyMock.expectLastCall(); + EasyMock.expect(secondRunner.getProcess()).andReturn(process); + secondRunner.waitFor(Duration.ofMinutes(1)); + EasyMock.expectLastCall().andReturn(0); + EasyMock.replay(process, secondRunner); + TestEmulatorHelper helper = + new TestEmulatorHelper(ImmutableList.of(firstRunner, secondRunner), BLOCK_UNTIL); + helper.start(); + helper.stop(Duration.ofMinutes(1)); + EasyMock.verify(); + } + + /** + * URLStreamHandler has a protected method which needs to be mocked, so we need our own + * implementation in this package + */ + private class MockURLStreamHandler extends URLStreamHandler { + @Override + protected URLConnection openConnection(URL u) throws IOException { + return null; + } + + @Override + protected String toExternalForm(URL u) { + return null; + } + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/testing/BlockingProcessStreamReaderTest.java b/google-cloud-core/src/test/java/com/google/cloud/testing/BlockingProcessStreamReaderTest.java new file mode 100644 index 0000000000..56b406f963 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/testing/BlockingProcessStreamReaderTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.testing; + +import static org.junit.Assert.assertEquals; + +import com.google.api.client.util.Charsets; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; + +public class BlockingProcessStreamReaderTest { + + private static final String BLOCK_UNTIL = "Dev App Server is now running"; + private static final String OUTPUT = "First Line\n" + "Second Line\n" + BLOCK_UNTIL; + private static final String OUTPUT_WITH_LOGS = + "First Line\n" + + BLOCK_UNTIL + + "\n" + + "Nov 08, 2016 2:05:44 PM io.netty.buffer.PooledByteBufAllocator \n" + + "INFO: log line 1\n" + + "log line 2\n" + + "Nov 08, 2016 2:05:44 PM io.netty.buffer.PooledByteBufAllocator \n" + + "FINE: log line 3\n"; + private static final String TAGGED_OUTPUT_WITH_LOGS = + "[emulator] First Line\n" + + "[emulator]" + + BLOCK_UNTIL + + "\n" + + "[emulator] Nov 08, 2016 2:05:44 PM io.netty.buffer.PooledByteBufAllocator \n" + + "[emulator] INFO: log line 1\n" + + "[emulator] log line 2\n" + + "[emulator] Nov 08, 2016 2:05:44 PM io.netty.buffer.PooledByteBufAllocator \n" + + "[emulator] FINE: log line 3\n"; + + @Rule public Timeout globalTimeout = Timeout.seconds(10); + + private static final class TestLogger extends Logger { + + private final Multimap logs = LinkedHashMultimap.create(); + + private TestLogger() { + super("text-logger", null); + } + + public void log(Level level, String msg) { + logs.put(level, msg); + } + + Multimap getLogs() { + return logs; + } + } + + @Test + public void testForwardLogEntry() throws IOException, InterruptedException { + TestLogger logger = new TestLogger(); + InputStream stream = new ByteArrayInputStream(OUTPUT_WITH_LOGS.getBytes(Charsets.UTF_8)); + BlockingProcessStreamReader.start("emulator", stream, BLOCK_UNTIL, logger).join(); + assertEquals( + "[emulator] log line 1" + System.lineSeparator() + "[emulator] log line 2", + logger.getLogs().get(Level.INFO).iterator().next()); + assertEquals("[emulator] log line 3", logger.getLogs().get(Level.FINE).iterator().next()); + stream.close(); + } + + @Test + public void testForwardAlreadyTaggedLogs() throws IOException, InterruptedException { + TestLogger logger = new TestLogger(); + InputStream stream = new ByteArrayInputStream(TAGGED_OUTPUT_WITH_LOGS.getBytes(Charsets.UTF_8)); + BlockingProcessStreamReader.start("emulator", stream, BLOCK_UNTIL, logger).join(); + assertEquals( + "[emulator] log line 1" + System.lineSeparator() + "[emulator] log line 2", + logger.getLogs().get(Level.INFO).iterator().next()); + assertEquals("[emulator] log line 3", logger.getLogs().get(Level.FINE).iterator().next()); + stream.close(); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/testing/CommandWrapperTest.java b/google-cloud-core/src/test/java/com/google/cloud/testing/CommandWrapperTest.java new file mode 100644 index 0000000000..88b246c754 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/testing/CommandWrapperTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.testing; + +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 java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import org.junit.Test; + +public class CommandWrapperTest { + + private static final List COMMAND = ImmutableList.of("my", "command"); + private static final List WIN_COMMAND = ImmutableList.of("cmd", "/C", "my", "command"); + private static final List UNIX_COMMAND = ImmutableList.of("bash", "my", "command"); + private static final Path DIRECTORY = Paths.get("my-path"); + private static final File WIN_NULL_FILE = new File("NUL:"); + private static final File UNIX_NULL_FILE = new File("/dev/null"); + + @Test + public void testCommandWrapperCommand() { + CommandWrapper commandWrapper = CommandWrapper.create(); + commandWrapper.setCommand(COMMAND); + ProcessBuilder processBuilder = commandWrapper.getBuilder(); + if (BaseEmulatorHelper.isWindows()) { + assertEquals(WIN_COMMAND, processBuilder.command()); + } else { + assertEquals(UNIX_COMMAND, processBuilder.command()); + } + assertNull(processBuilder.directory()); + assertFalse(processBuilder.redirectErrorStream()); + assertEquals(ProcessBuilder.Redirect.PIPE, processBuilder.redirectError()); + } + + @Test + public void testCommandWrapperRedirectErrorStream() { + CommandWrapper commandWrapper = CommandWrapper.create(); + commandWrapper.setCommand(COMMAND); + commandWrapper.setRedirectErrorStream(); + ProcessBuilder processBuilder = commandWrapper.getBuilder(); + if (BaseEmulatorHelper.isWindows()) { + assertEquals(WIN_COMMAND, processBuilder.command()); + } else { + assertEquals(UNIX_COMMAND, processBuilder.command()); + } + assertNull(processBuilder.directory()); + assertTrue(processBuilder.redirectErrorStream()); + assertEquals(ProcessBuilder.Redirect.PIPE, processBuilder.redirectError()); + } + + @Test + public void testCommandWrapperRedirectErrorInherit() { + CommandWrapper commandWrapper = CommandWrapper.create(); + commandWrapper.setCommand(COMMAND); + commandWrapper.setRedirectErrorInherit(); + ProcessBuilder processBuilder = commandWrapper.getBuilder(); + if (BaseEmulatorHelper.isWindows()) { + assertEquals(WIN_COMMAND, processBuilder.command()); + } else { + assertEquals(UNIX_COMMAND, processBuilder.command()); + } + assertNull(processBuilder.directory()); + assertFalse(processBuilder.redirectErrorStream()); + assertEquals(ProcessBuilder.Redirect.INHERIT, processBuilder.redirectError()); + } + + @Test + public void testCommandWrapperDirectory() { + CommandWrapper commandWrapper = CommandWrapper.create(); + commandWrapper.setCommand(COMMAND); + commandWrapper.setDirectory(DIRECTORY); + ProcessBuilder processBuilder = commandWrapper.getBuilder(); + if (BaseEmulatorHelper.isWindows()) { + assertEquals(WIN_COMMAND, processBuilder.command()); + } else { + assertEquals(UNIX_COMMAND, processBuilder.command()); + } + assertEquals(DIRECTORY, processBuilder.directory().toPath()); + assertFalse(processBuilder.redirectErrorStream()); + assertEquals(ProcessBuilder.Redirect.PIPE, processBuilder.redirectError()); + } + + @Test + public void testCommandWrapperRedirectOutputToNull() { + CommandWrapper commandWrapper = CommandWrapper.create(); + commandWrapper.setCommand(COMMAND); + commandWrapper.setRedirectOutputToNull(); + ProcessBuilder processBuilder = commandWrapper.getBuilder(); + if (BaseEmulatorHelper.isWindows()) { + assertEquals(WIN_COMMAND, processBuilder.command()); + assertEquals(ProcessBuilder.Redirect.to(WIN_NULL_FILE), processBuilder.redirectOutput()); + } else { + assertEquals(UNIX_COMMAND, processBuilder.command()); + assertEquals(ProcessBuilder.Redirect.to(UNIX_NULL_FILE), processBuilder.redirectOutput()); + } + assertNull(processBuilder.directory()); + assertFalse(processBuilder.redirectErrorStream()); + assertEquals(ProcessBuilder.Redirect.PIPE, processBuilder.redirectError()); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/testing/VersionTest.java b/google-cloud-core/src/test/java/com/google/cloud/testing/VersionTest.java new file mode 100644 index 0000000000..4e1d88f8a2 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/testing/VersionTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.testing; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class VersionTest { + + @Rule public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testFromString() { + Version version = Version.fromString("2016.01.13"); + assertEquals(2016, version.getMajor()); + assertEquals(1, version.getMinor()); + assertEquals(13, version.getPatch()); + version = Version.fromString("1.2.0"); + assertEquals(1, version.getMajor()); + assertEquals(2, version.getMinor()); + assertEquals(0, version.getPatch()); + } + + @Test + public void testFromStringWithAlphas() { + thrown.expect(IllegalArgumentException.class); + Version.fromString("2016.01.hello"); + } + + @Test + public void testFromStringMissingPatch() { + thrown.expect(IllegalArgumentException.class); + Version.fromString("2016.01"); + } + + @Test + public void testFromStringMissingMinor() { + thrown.expect(IllegalArgumentException.class); + Version.fromString("2016"); + } + + @Test + public void testFromStringEmpty() { + thrown.expect(IllegalArgumentException.class); + Version.fromString(""); + } + + @Test + public void testFromStringNull() { + thrown.expect(NullPointerException.class); + Version.fromString(null); + } + + @Test + public void testCompare() { + Version version = Version.fromString("2016.01.13"); + Version sameVersion = Version.fromString("2016.01.13"); + Version olderVersion = Version.fromString("2015.12.01"); + Version newerVersion = Version.fromString("2016.08.12"); + assertEquals(0, version.compareTo(sameVersion)); + assertTrue(version.compareTo(olderVersion) > 0); + assertTrue(version.compareTo(newerVersion) < 0); + Version otherVersion = Version.fromString("1.2.0"); + assertTrue(version.compareTo(otherVersion) > 0); + } +} diff --git a/java.header b/java.header new file mode 100644 index 0000000000..3a9b503aa2 --- /dev/null +++ b/java.header @@ -0,0 +1,15 @@ +^/\*$ +^ \* Copyright \d\d\d\d,? Google (Inc\.|LLC)( All [rR]ights [rR]eserved\.)?$ +^ \*$ +^ \* 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$ +^ \*$ +^ \*[ ]+https?://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/license-checks.xml b/license-checks.xml new file mode 100644 index 0000000000..6597fced80 --- /dev/null +++ b/license-checks.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000..116100ab60 --- /dev/null +++ b/pom.xml @@ -0,0 +1,413 @@ + + + 4.0.0 + com.google.cloud + google-cloud-core-parent + pom + 1.91.0 + Google Cloud Core Parent + https://github.com/googleapis/java-core + + Java idiomatic client for Google Cloud Platform services. + + + + com.google.cloud + google-cloud-shared-config + 0.1.2 + + + + + garrettjonesgoogle + Garrett Jones + garrettjones@google.com + Google + + Developer + + + + pongad + Michael Darakananda + pongad@google.com + Google + + Developer + + + + shinfan + Shin Fan + shinfan@google.com + Google + + Developer + + + + michaelbausor + Micheal Bausor + michaelbausor@google.com + Google + + Developer + + + + vam-google + Vadym Matsishevskyi + vam@google.com + Google + + Developer + + + + tswast + Tim Swast + tswast@google.com + Google + + Developer + + + + neozwu + Neo Wu + neowu@google.com + Google + + Developer + + + + lesv + Les Vogel + lesv@google.com + Google + + Developer + + + + schmidt_sebastian + Sebastian Schmidt + mrschmidt@google.com + Google + + Developer + + + + andreamlin + Andrea Lin + andrealin@google.com + + Developer + + + + hzyi-google + Hanzhen Yi + hzyi@google.com + + Developer + + + + + Google LLC + + + scm:git:git@github.com:googleapis/java-core.git + scm:git:git@github.com:googleapis/java-core.git + https://github.com/googleapis/java-core + HEAD + + + https://github.com/googleapis/java-core/issues + GitHub Issues + + + + sonatype-nexus-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + + sonatype-nexus-staging + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + UTF-8 + UTF-8 + github + google-cloud-core-parent + + 1.48.1 + 1.8.1 + 1.16.0 + 0.12.0 + 0.17.1 + 1.30.3 + 1.32.0 + 1.23.0 + 3.9.1 + 0.24.0 + 1.3.2 + 28.0-android + 4.12 + 1.0 + 3.6 + 3.0.2 + 1.3.3 + 2.6 + 2.3.2 + 1.6.6 + 2.8.5 + + + + + + com.google.cloud + google-cloud-core + ${project.version} + + + + com.google.auth + google-auth-library-bom + ${google.auth.version} + pom + import + + + com.google.api + gax-bom + ${gax.version} + pom + import + + + com.google.http-client + google-http-client-bom + ${google.http.version} + pom + import + + + com.google.api-client + google-api-client-bom + ${google.api.version} + pom + import + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + com.google.protobuf + protobuf-bom + ${protobuf.version} + pom + import + + + com.google.guava + guava-bom + ${guava.version} + pom + import + + + + com.google.api + api-common + ${google.api-common.version} + + + com.google.api.grpc + proto-google-common-protos + ${google.common-protos.version} + + + com.google.api.grpc + proto-google-iam-v1 + ${google.iam.version} + + + + + io.opencensus + opencensus-api + ${opencensus.version} + + + io.opencensus + opencensus-contrib-http-util + ${opencensus.version} + + + + javax.annotation + javax.annotation-api + ${annotations-api.version} + + + com.google.code.findbugs + jsr305 + ${findbugs.version} + + + org.threeten + threetenbp + ${threetenbp.version} + + + com.google.errorprone + error_prone_annotations + ${errorprone.version} + + + com.google.auto.value + auto-value-annotations + ${autovalue.version} + + + com.google.code.gson + gson + ${gson.version} + + + + + com.google.truth + truth + ${truth.version} + test + + + junit + junit + ${junit.version} + test + + + org.easymock + easymock + ${easymock.version} + test + + + org.objenesis + objenesis + ${objenesis.version} + test + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + org.objenesis:objenesis + + + + + + + + google-cloud-core + google-cloud-core-http + google-cloud-core-grpc + google-cloud-core-bom + + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.0.0 + + + + index + dependency-info + team + ci-management + issue-management + licenses + scm + dependency-management + distribution-management + summary + modules + + + + + true + ${site.installationModule} + jar + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.1 + + + html + + aggregate + javadoc + + + + + none + protected + true + ${project.build.directory}/javadoc + + + Test helpers packages + com.google.cloud.testing + + + SPI packages + com.google.cloud.spi* + + + + + https://grpc.io/grpc-java/javadoc/ + https://developers.google.com/protocol-buffers/docs/reference/java/ + https://googleapis.dev/java/google-auth-library/latest/ + https://googleapis.dev/java/gax/latest/ + https://googleapis.github.io/api-common-java/${google.api-common.version}/apidocs/ + + + + + + diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000000..dae8a245c8 --- /dev/null +++ b/renovate.json @@ -0,0 +1,31 @@ +{ + "extends": [ + "config:base" + ], + "ignoreDeps": [], + "packageRules": [ + { + "packagePatterns": ["*"], + "semanticCommitType": "chore" + }, + { + "depTypeList": [ + "dependencies" + ], + "semanticCommitType": "deps" + }, + { + "packagePatterns": ["^io.grpc:grpc-"], + "groupName": "gRPC packages" + }, + { + "packagePatterns": ["^com.google.protobuf:protobuf-"], + "groupName": "Protobuf packages" + }, + { + "packagePatterns": ["^io.opencensus:opencensus-"], + "groupName": "OpenCensus packages" + } + ], + "semanticCommits": true +} diff --git a/synth.metadata b/synth.metadata new file mode 100644 index 0000000000..4d753071d0 --- /dev/null +++ b/synth.metadata @@ -0,0 +1,12 @@ +{ + "updateTime": "2019-09-17T08:01:41.230045Z", + "sources": [ + { + "template": { + "name": "java_library", + "origin": "synthtool.gcp", + "version": "2019.5.2" + } + } + ] +} \ No newline at end of file diff --git a/synth.py b/synth.py new file mode 100644 index 0000000000..b3e7b1e030 --- /dev/null +++ b/synth.py @@ -0,0 +1,27 @@ +# Copyright 2019 Google LLC +# +# 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. + +"""This script is used to synthesize generated parts of this library.""" + +import synthtool as s +import synthtool.gcp as gcp +import logging +logging.basicConfig(level=logging.DEBUG) +common_templates = gcp.CommonTemplates() +templates = common_templates.java_library() +s.copy(templates, excludes=[ + '.gitignore', + 'README.md', +]) + diff --git a/versions.txt b/versions.txt new file mode 100644 index 0000000000..a4990a4eb7 --- /dev/null +++ b/versions.txt @@ -0,0 +1,4 @@ +# Format: +# module:released-version:current-version + +google-cloud-core:1.91.0:1.91.0 \ No newline at end of file