From 22ae07f51e144baa27ddc56d56a8c6ac6f63ad58 Mon Sep 17 00:00:00 2001 From: Tomasz Marciniak Date: Wed, 8 Aug 2018 13:45:06 +0200 Subject: [PATCH] Arvados SDK Java - release 2.0.0 Arvados-DCO-1.1-Signed-off-by: Tomasz Marciniak --- .gitignore | 9 + README.md | 115 +++ build.gradle | 50 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54727 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 ++++ gradlew.bat | 84 ++ settings.gradle | 1 + .../client/api/client/BaseApiClient.java | 83 ++ .../api/client/BaseStandardApiClient.java | 153 +++ .../api/client/CollectionsApiClient.java | 45 + .../api/client/CountingFileRequestBody.java | 84 ++ .../client/api/client/GroupsApiClient.java | 61 ++ .../api/client/KeepServerApiClient.java | 54 ++ .../api/client/KeepServicesApiClient.java | 43 + .../client/api/client/KeepWebApiClient.java | 37 + .../client/api/client/ProgressListener.java | 14 + .../client/api/client/UsersApiClient.java | 51 + .../client/factory/OkHttpClientFactory.java | 89 ++ .../arvados/client/api/model/ApiError.java | 42 + .../arvados/client/api/model/Collection.java | 137 +++ .../client/api/model/CollectionList.java | 32 + .../org/arvados/client/api/model/Group.java | 319 +++++++ .../arvados/client/api/model/GroupList.java | 32 + .../org/arvados/client/api/model/Item.java | 123 +++ .../arvados/client/api/model/ItemList.java | 80 ++ .../arvados/client/api/model/KeepService.java | 77 ++ .../client/api/model/KeepServiceList.java | 32 + .../client/api/model/RuntimeConstraints.java | 60 ++ .../org/arvados/client/api/model/User.java | 147 +++ .../arvados/client/api/model/UserList.java | 32 + .../client/api/model/argument/Argument.java | 24 + .../api/model/argument/ContentsGroup.java | 63 ++ .../client/api/model/argument/Filter.java | 118 +++ .../api/model/argument/ListArgument.java | 123 +++ .../api/model/argument/UntrashGroup.java | 28 + .../org/arvados/client/common/Characters.java | 21 + .../org/arvados/client/common/Headers.java | 15 + .../org/arvados/client/common/Patterns.java | 19 + .../arvados/client/config/ConfigProvider.java | 40 + .../client/config/ExternalConfigProvider.java | 181 ++++ .../client/config/FileConfigProvider.java | 107 +++ .../client/exception/ArvadosApiException.java | 25 + .../exception/ArvadosClientException.java | 27 + .../arvados/client/facade/ArvadosFacade.java | 299 ++++++ .../logic/collection/CollectionFactory.java | 134 +++ .../client/logic/collection/FileToken.java | 60 ++ .../logic/collection/ManifestDecoder.java | 74 ++ .../logic/collection/ManifestFactory.java | 67 ++ .../logic/collection/ManifestStream.java | 45 + .../client/logic/keep/FileDownloader.java | 256 +++++ .../logic/keep/FileTransferHandler.java | 55 ++ .../client/logic/keep/FileUploader.java | 101 ++ .../arvados/client/logic/keep/KeepClient.java | 244 +++++ .../client/logic/keep/KeepLocator.java | 86 ++ .../DownloadFolderAlreadyExistsException.java | 24 + .../exception/FileAlreadyExistsException.java | 23 + .../org/arvados/client/utils/FileMerge.java | 26 + .../org/arvados/client/utils/FileSplit.java | 44 + src/main/resources/reference.conf | 23 + .../api/client/BaseStandardApiClientTest.java | 50 + .../api/client/CollectionsApiClientTest.java | 112 +++ .../api/client/GroupsApiClientTest.java | 95 ++ .../api/client/KeepServerApiClientTest.java | 70 ++ .../api/client/KeepServicesApiClientTest.java | 78 ++ .../client/api/client/UsersApiClientTest.java | 126 +++ .../factory/OkHttpClientFactoryTest.java | 96 ++ .../facade/ArvadosFacadeIntegrationTest.java | 258 ++++++ .../client/facade/ArvadosFacadeTest.java | 198 ++++ .../junit/categories/IntegrationTests.java | 10 + .../logic/collection/FileTokenTest.java | 42 + .../logic/collection/ManifestDecoderTest.java | 108 +++ .../logic/collection/ManifestFactoryTest.java | 38 + .../logic/collection/ManifestStreamTest.java | 27 + .../client/logic/keep/FileDownloaderTest.java | 145 +++ .../client/logic/keep/KeepClientTest.java | 118 +++ .../client/logic/keep/KeepLocatorTest.java | 57 ++ .../client/test/utils/ApiClientTestUtils.java | 45 + .../utils/ArvadosClientIntegrationTest.java | 28 + .../ArvadosClientMockedWebServerTest.java | 27 + .../test/utils/ArvadosClientUnitTest.java | 24 + .../client/test/utils/FileTestUtils.java | 50 + .../client/test/utils/RequestMethod.java | 13 + .../arvados/client/utils/FileMergeTest.java | 48 + .../arvados/client/utils/FileSplitTest.java | 48 + src/test/resources/application.conf | 10 + .../integration-tests-application.conf | 23 + ...integration-tests-application.conf.example | 16 + .../org.mockito.plugins.MockMaker | 1 + .../client/collections-create-manifest.json | 22 + .../api/client/collections-create-simple.json | 22 + .../api/client/collections-download-file.json | 22 + .../client/api/client/collections-get.json | 22 + .../client/api/client/collections-list.json | 871 ++++++++++++++++++ .../arvados/client/api/client/groups-get.json | 21 + .../client/api/client/groups-list.json | 430 +++++++++ .../api/client/keep-client-test-file.txt | 1 + .../keep-services-accessible-disk-only.json | 42 + .../api/client/keep-services-accessible.json | 42 + .../client/api/client/keep-services-get.json | 16 + .../client/api/client/keep-services-list.json | 58 ++ .../client/keep-services-not-accessible.json | 9 + .../client/api/client/users-create.json | 26 + .../arvados/client/api/client/users-get.json | 26 + .../arvados/client/api/client/users-list.json | 115 +++ .../client/api/client/users-system.json | 24 + src/test/resources/selfsigned.keystore.jks | Bin 0 -> 2247 bytes 107 files changed, 8345 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/org/arvados/client/api/client/BaseApiClient.java create mode 100644 src/main/java/org/arvados/client/api/client/BaseStandardApiClient.java create mode 100644 src/main/java/org/arvados/client/api/client/CollectionsApiClient.java create mode 100644 src/main/java/org/arvados/client/api/client/CountingFileRequestBody.java create mode 100644 src/main/java/org/arvados/client/api/client/GroupsApiClient.java create mode 100644 src/main/java/org/arvados/client/api/client/KeepServerApiClient.java create mode 100644 src/main/java/org/arvados/client/api/client/KeepServicesApiClient.java create mode 100644 src/main/java/org/arvados/client/api/client/KeepWebApiClient.java create mode 100644 src/main/java/org/arvados/client/api/client/ProgressListener.java create mode 100644 src/main/java/org/arvados/client/api/client/UsersApiClient.java create mode 100644 src/main/java/org/arvados/client/api/client/factory/OkHttpClientFactory.java create mode 100644 src/main/java/org/arvados/client/api/model/ApiError.java create mode 100644 src/main/java/org/arvados/client/api/model/Collection.java create mode 100644 src/main/java/org/arvados/client/api/model/CollectionList.java create mode 100644 src/main/java/org/arvados/client/api/model/Group.java create mode 100644 src/main/java/org/arvados/client/api/model/GroupList.java create mode 100644 src/main/java/org/arvados/client/api/model/Item.java create mode 100644 src/main/java/org/arvados/client/api/model/ItemList.java create mode 100644 src/main/java/org/arvados/client/api/model/KeepService.java create mode 100644 src/main/java/org/arvados/client/api/model/KeepServiceList.java create mode 100644 src/main/java/org/arvados/client/api/model/RuntimeConstraints.java create mode 100644 src/main/java/org/arvados/client/api/model/User.java create mode 100644 src/main/java/org/arvados/client/api/model/UserList.java create mode 100644 src/main/java/org/arvados/client/api/model/argument/Argument.java create mode 100644 src/main/java/org/arvados/client/api/model/argument/ContentsGroup.java create mode 100644 src/main/java/org/arvados/client/api/model/argument/Filter.java create mode 100644 src/main/java/org/arvados/client/api/model/argument/ListArgument.java create mode 100644 src/main/java/org/arvados/client/api/model/argument/UntrashGroup.java create mode 100644 src/main/java/org/arvados/client/common/Characters.java create mode 100644 src/main/java/org/arvados/client/common/Headers.java create mode 100644 src/main/java/org/arvados/client/common/Patterns.java create mode 100644 src/main/java/org/arvados/client/config/ConfigProvider.java create mode 100644 src/main/java/org/arvados/client/config/ExternalConfigProvider.java create mode 100644 src/main/java/org/arvados/client/config/FileConfigProvider.java create mode 100644 src/main/java/org/arvados/client/exception/ArvadosApiException.java create mode 100644 src/main/java/org/arvados/client/exception/ArvadosClientException.java create mode 100644 src/main/java/org/arvados/client/facade/ArvadosFacade.java create mode 100644 src/main/java/org/arvados/client/logic/collection/CollectionFactory.java create mode 100644 src/main/java/org/arvados/client/logic/collection/FileToken.java create mode 100644 src/main/java/org/arvados/client/logic/collection/ManifestDecoder.java create mode 100644 src/main/java/org/arvados/client/logic/collection/ManifestFactory.java create mode 100644 src/main/java/org/arvados/client/logic/collection/ManifestStream.java create mode 100644 src/main/java/org/arvados/client/logic/keep/FileDownloader.java create mode 100644 src/main/java/org/arvados/client/logic/keep/FileTransferHandler.java create mode 100644 src/main/java/org/arvados/client/logic/keep/FileUploader.java create mode 100644 src/main/java/org/arvados/client/logic/keep/KeepClient.java create mode 100644 src/main/java/org/arvados/client/logic/keep/KeepLocator.java create mode 100644 src/main/java/org/arvados/client/logic/keep/exception/DownloadFolderAlreadyExistsException.java create mode 100644 src/main/java/org/arvados/client/logic/keep/exception/FileAlreadyExistsException.java create mode 100644 src/main/java/org/arvados/client/utils/FileMerge.java create mode 100644 src/main/java/org/arvados/client/utils/FileSplit.java create mode 100644 src/main/resources/reference.conf create mode 100644 src/test/java/org/arvados/client/api/client/BaseStandardApiClientTest.java create mode 100644 src/test/java/org/arvados/client/api/client/CollectionsApiClientTest.java create mode 100644 src/test/java/org/arvados/client/api/client/GroupsApiClientTest.java create mode 100644 src/test/java/org/arvados/client/api/client/KeepServerApiClientTest.java create mode 100644 src/test/java/org/arvados/client/api/client/KeepServicesApiClientTest.java create mode 100644 src/test/java/org/arvados/client/api/client/UsersApiClientTest.java create mode 100644 src/test/java/org/arvados/client/api/client/factory/OkHttpClientFactoryTest.java create mode 100644 src/test/java/org/arvados/client/facade/ArvadosFacadeIntegrationTest.java create mode 100644 src/test/java/org/arvados/client/facade/ArvadosFacadeTest.java create mode 100644 src/test/java/org/arvados/client/junit/categories/IntegrationTests.java create mode 100644 src/test/java/org/arvados/client/logic/collection/FileTokenTest.java create mode 100644 src/test/java/org/arvados/client/logic/collection/ManifestDecoderTest.java create mode 100644 src/test/java/org/arvados/client/logic/collection/ManifestFactoryTest.java create mode 100644 src/test/java/org/arvados/client/logic/collection/ManifestStreamTest.java create mode 100644 src/test/java/org/arvados/client/logic/keep/FileDownloaderTest.java create mode 100644 src/test/java/org/arvados/client/logic/keep/KeepClientTest.java create mode 100644 src/test/java/org/arvados/client/logic/keep/KeepLocatorTest.java create mode 100644 src/test/java/org/arvados/client/test/utils/ApiClientTestUtils.java create mode 100644 src/test/java/org/arvados/client/test/utils/ArvadosClientIntegrationTest.java create mode 100644 src/test/java/org/arvados/client/test/utils/ArvadosClientMockedWebServerTest.java create mode 100644 src/test/java/org/arvados/client/test/utils/ArvadosClientUnitTest.java create mode 100644 src/test/java/org/arvados/client/test/utils/FileTestUtils.java create mode 100644 src/test/java/org/arvados/client/test/utils/RequestMethod.java create mode 100644 src/test/java/org/arvados/client/utils/FileMergeTest.java create mode 100644 src/test/java/org/arvados/client/utils/FileSplitTest.java create mode 100644 src/test/resources/application.conf create mode 100644 src/test/resources/integration-tests-application.conf create mode 100644 src/test/resources/integration-tests-application.conf.example create mode 100644 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 src/test/resources/org/arvados/client/api/client/collections-create-manifest.json create mode 100644 src/test/resources/org/arvados/client/api/client/collections-create-simple.json create mode 100644 src/test/resources/org/arvados/client/api/client/collections-download-file.json create mode 100644 src/test/resources/org/arvados/client/api/client/collections-get.json create mode 100644 src/test/resources/org/arvados/client/api/client/collections-list.json create mode 100644 src/test/resources/org/arvados/client/api/client/groups-get.json create mode 100644 src/test/resources/org/arvados/client/api/client/groups-list.json create mode 100644 src/test/resources/org/arvados/client/api/client/keep-client-test-file.txt create mode 100644 src/test/resources/org/arvados/client/api/client/keep-services-accessible-disk-only.json create mode 100644 src/test/resources/org/arvados/client/api/client/keep-services-accessible.json create mode 100644 src/test/resources/org/arvados/client/api/client/keep-services-get.json create mode 100644 src/test/resources/org/arvados/client/api/client/keep-services-list.json create mode 100644 src/test/resources/org/arvados/client/api/client/keep-services-not-accessible.json create mode 100644 src/test/resources/org/arvados/client/api/client/users-create.json create mode 100644 src/test/resources/org/arvados/client/api/client/users-get.json create mode 100644 src/test/resources/org/arvados/client/api/client/users-list.json create mode 100644 src/test/resources/org/arvados/client/api/client/users-system.json create mode 100644 src/test/resources/selfsigned.keystore.jks diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..c928081f78 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/.gradle/ +/bin/ +/build/ +.project +.classpath +/.settings/ +.DS_Store +/.idea/ +/out/ diff --git a/README.md b/README.md new file mode 100644 index 0000000000..ca5aef91c1 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +``` +Copyright (C) The Arvados Authors. All rights reserved. + +SPDX-License-Identifier: CC-BY-SA-3.0 +``` + +# Arvados Java SDK + +##### About +Arvados Java Client allows to access Arvados servers and uses two APIs: +* lower level [Keep Server API](https://doc.arvados.org/api/index.html) +* higher level [Keep-Web API](https://godoc.org/github.com/curoverse/arvados/services/keep-web) (when needed) + +##### Required Java version +This SDK requires Java 8+ + +##### Logging + +SLF4J is used for logging. Concrete logging framework and configuration must be provided by a client. + +##### Configuration + +[TypeSafe Configuration](https://github.com/lightbend/config) is used for configuring this library. + +Please, have a look at java/resources/reference.conf for default values provided with this library. + +* **keepweb-host** - change to host of your Keep-Web installation +* **keepweb-port** - change to port of your Keep-Web installation +* **host** - change to host of your Arvados installation +* **port** - change to port of your Arvados installation +* **token** - authenticates registered user, one must provide + [token obtained from Arvados Workbench](https://doc.arvados.org/user/reference/api-tokens.html) +* **protocol** - don't change to unless really needed +* **host-insecure** - insecure communication with Arvados (ignores SSL certificate verification), + don't change to *true* unless really needed +* **split-size** - size of chunk files in megabytes +* **temp-dir** - temporary chunk files storage +* **copies** - amount of chunk files duplicates per Keep server +* **retries** - in case of chunk files send failure this should allow to repeat send + (*NOTE*: this parameter is not used at the moment but was left for future improvements) + +In order to override default settings one can create application.conf file in an application. +Example: src/test/resources/application.conf. + +Alternatively ExternalConfigProvider class can be used to pass configuration via code. +ExternalConfigProvider comes with a builder and all of the above values must be provided in order for it to work properly. + +ArvadosFacade has two constructors, one without arguments that uses values from reference.conf and second one +taking ExternalConfigProvider as an argument. + +##### API clients + +All API clients inherit from BaseStandardApiClient. This class contains implementation of all +common methods as described in http://doc.arvados.org/api/methods.html. + +Parameters provided to common or specific methods are String UUID or fields wrapped in Java objects. For example: + +```java +String uuid = "ardev-4zz18-rxcql7qwyakg1r1"; + +Collection actual = client.get(uuid); +``` + +```java +ListArgument listArgument = ListArgument.builder() + .filters(Arrays.asList( + Filter.of("owner_uuid", Operator.LIKE, "ardev%"), + Filter.of("name", Operator.LIKE, "Super%"), + Filter.of("portable_data_hash", Operator.IN, Lists.newArrayList("54f6d9f59065d3c009d4306660989379+65") + ))) + .build(); + +CollectionList actual = client.list(listArgument); +``` + +Non-standard API clients must inherit from BaseApiClient. +For example: KeepServerApiClient communicates directly with Keep servers using exclusively non-common methods. + +##### Business logic + +More advanced API data handling could be implemented as *Facade* classes. +In current version functionalities provided by SDK are handled by *ArvadosFacade*. +They include: +* **downloading single file from collection** - using Keep-Web +* **downloading whole collection** - using Keep-Web or Keep Server API +* **listing file info from certain collection** - information is returned as list of *FileTokens* providing file details +* **uploading single file** - to either new or existing collection +* **uploading list of files** - to either new or existing collection +* **creating an empty collection** +* **getting current user info** +* **listing current user's collections** +* **creating new project** +* **deleting certain collection** + +##### Note regarding Keep-Web + +Current version requires both Keep Web and standard Keep Server API configured in order to use Keep-Web functionalities. + +##### Integration tests + +In order to run integration tests all fields within following configuration file must be provided: +```java +src/test/resources/integration-test-appliation.conf +``` +Parameter **integration-tests.project-uuid** should contain UUID of one project available to user, +whose token was provided within configuration file. + +Integration tests require connection to real Arvados server. + +##### Note regarding file naming + +While uploading via this SDK all uploaded files within single collection must have different names. +This applies also to uploading files to already existing collection. +Renaming files with duplicate names is not implemented in current version. + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..eeec33369b --- /dev/null +++ b/build.gradle @@ -0,0 +1,50 @@ +apply plugin: 'java-library' +apply plugin: 'eclipse' +apply plugin: 'idea' +apply plugin: 'maven' + +version = '2.0.0' + +repositories { + mavenCentral() +} + +dependencies { + api 'com.squareup.okhttp3:okhttp:3.9.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.9.2' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.9.2' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.2' + api 'commons-codec:commons-codec:1.11' + api 'commons-io:commons-io:2.6' + api 'com.google.guava:guava:23.4-jre' + api 'org.slf4j:slf4j-api:1.7.25' + api 'com.typesafe:config:1.3.2' + + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:2.12.0' + testImplementation 'org.assertj:assertj-core:3.8.0' + testImplementation 'com.squareup.okhttp3:mockwebserver:3.9.1' +} + +test { + useJUnit { + excludeCategories 'org.arvados.client.junit.categories.IntegrationTests' + } + + testLogging { + events "passed", "skipped", "failed" + afterSuite { desc, result -> + if (!desc.parent) { // will match the outermost suite + println "\n---- Test results ----" + println "${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" + println "" + } + } + } +} + +task integrationTest(type: Test) { + useJUnit { + includeCategories 'org.arvados.client.junit.categories.IntegrationTests' + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..27768f1bbac3ce2d055b20d521f12da78d331e8e GIT binary patch literal 54727 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

i| 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..9d2dc020a2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..cccdd3d517 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..f9553162f1 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..be8ccc6ca4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'arvados-java' diff --git a/src/main/java/org/arvados/client/api/client/BaseApiClient.java b/src/main/java/org/arvados/client/api/client/BaseApiClient.java new file mode 100644 index 0000000000..7e8a2979be --- /dev/null +++ b/src/main/java/org/arvados/client/api/client/BaseApiClient.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.arvados.client.exception.ArvadosApiException; +import org.arvados.client.api.client.factory.OkHttpClientFactory; +import org.arvados.client.api.model.ApiError; +import org.arvados.client.config.ConfigProvider; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +abstract class BaseApiClient { + + static final ObjectMapper MAPPER = new ObjectMapper().findAndRegisterModules(); + + final OkHttpClient client; + final ConfigProvider config; + private final Logger log = org.slf4j.LoggerFactory.getLogger(BaseApiClient.class); + + BaseApiClient(ConfigProvider config) { + this.config = config; + client = OkHttpClientFactory.builder() + .build() + .create(config.isApiHostInsecure()); + } + + Request.Builder getRequestBuilder() { + return new Request.Builder() + .addHeader("authorization", String.format("OAuth2 %s", config.getApiToken())) + .addHeader("cache-control", "no-cache"); + } + + String newCall(Request request) { + return (String) getResponseBody(request, body -> body.string().trim()); + } + + byte[] newFileCall(Request request) { + return (byte[]) getResponseBody(request, ResponseBody::bytes); + } + + private Object getResponseBody(Request request, Command command) { + try { + log.debug(URLDecoder.decode(request.toString(), StandardCharsets.UTF_8.name())); + } catch (UnsupportedEncodingException e) { + throw new ArvadosApiException(e); + } + + try (Response response = client.newCall(request).execute()) { + ResponseBody responseBody = response.body(); + + if (!response.isSuccessful()) { + String errorBody = Objects.requireNonNull(responseBody).string(); + if (errorBody == null || errorBody.length() == 0) { + throw new ArvadosApiException(String.format("Error code %s with message: %s", response.code(), response.message())); + } + ApiError apiError = MAPPER.readValue(errorBody, ApiError.class); + throw new ArvadosApiException(String.format("Error code %s with messages: %s", response.code(), apiError.getErrors())); + } + return command.readResponseBody(responseBody); + } catch (IOException e) { + throw new ArvadosApiException(e); + } + } + + private interface Command { + Object readResponseBody(ResponseBody body) throws IOException; + } +} diff --git a/src/main/java/org/arvados/client/api/client/BaseStandardApiClient.java b/src/main/java/org/arvados/client/api/client/BaseStandardApiClient.java new file mode 100644 index 0000000000..ab03d34f19 --- /dev/null +++ b/src/main/java/org/arvados/client/api/client/BaseStandardApiClient.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectWriter; +import okhttp3.MediaType; +import okhttp3.HttpUrl; +import okhttp3.HttpUrl.Builder; +import okhttp3.Request; +import okhttp3.RequestBody; +import org.arvados.client.exception.ArvadosApiException; +import org.arvados.client.api.model.Item; +import org.arvados.client.api.model.ItemList; +import org.arvados.client.api.model.argument.ListArgument; +import org.arvados.client.config.ConfigProvider; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.Map; + +public abstract class BaseStandardApiClient extends BaseApiClient { + + private static final MediaType JSON = MediaType.parse(com.google.common.net.MediaType.JSON_UTF_8.toString()); + private final Logger log = org.slf4j.LoggerFactory.getLogger(BaseStandardApiClient.class); + + BaseStandardApiClient(ConfigProvider config) { + super(config); + } + + public L list(ListArgument listArguments) { + log.debug("Get list of {}", getType().getSimpleName()); + Builder urlBuilder = getUrlBuilder(); + addQueryParameters(urlBuilder, listArguments); + HttpUrl url = urlBuilder.build(); + Request request = getRequestBuilder().url(url).build(); + return callForList(request); + } + + public L list() { + return list(ListArgument.builder().build()); + } + + public T get(String uuid) { + log.debug("Get {} by UUID {}", getType().getSimpleName(), uuid); + HttpUrl url = getUrlBuilder().addPathSegment(uuid).build(); + Request request = getRequestBuilder().get().url(url).build(); + return callForType(request); + } + + public T create(T type) { + log.debug("Create {}", getType().getSimpleName()); + String json = mapToJson(type); + RequestBody body = RequestBody.create(JSON, json); + Request request = getRequestBuilder().post(body).build(); + return callForType(request); + } + + public T delete(String uuid) { + log.debug("Delete {} by UUID {}", getType().getSimpleName(), uuid); + HttpUrl url = getUrlBuilder().addPathSegment(uuid).build(); + Request request = getRequestBuilder().delete().url(url).build(); + return callForType(request); + } + + public T update(T type) { + String uuid = type.getUuid(); + log.debug("Update {} by UUID {}", getType().getSimpleName(), uuid); + String json = mapToJson(type); + RequestBody body = RequestBody.create(JSON, json); + HttpUrl url = getUrlBuilder().addPathSegment(uuid).build(); + Request request = getRequestBuilder().put(body).url(url).build(); + return callForType(request); + } + + @Override + Request.Builder getRequestBuilder() { + return super.getRequestBuilder().url(getUrlBuilder().build()); + } + + HttpUrl.Builder getUrlBuilder() { + return new HttpUrl.Builder() + .scheme(config.getApiProtocol()) + .host(config.getApiHost()) + .port(config.getApiPort()) + .addPathSegment("arvados") + .addPathSegment("v1") + .addPathSegment(getResource()); + } + + TL call(Request request, Class cls) { + String bodyAsString = newCall(request); + try { + return mapToObject(bodyAsString, cls); + } catch (IOException e) { + throw new ArvadosApiException("A problem occurred while parsing JSON data", e); + } + } + + private TL mapToObject(String content, Class cls) throws IOException { + return MAPPER.readValue(content, cls); + } + + private String mapToJson(TL type) { + ObjectWriter writer = MAPPER.writer().withDefaultPrettyPrinter(); + try { + return writer.writeValueAsString(type); + } catch (JsonProcessingException e) { + log.error(e.getMessage()); + return null; + } + } + + T callForType(Request request) { + return call(request, getType()); + } + + L callForList(Request request) { + return call(request, getListType()); + } + + abstract String getResource(); + + abstract Class getType(); + + abstract Class getListType(); + + Request getNoArgumentMethodRequest(String method) { + HttpUrl url = getUrlBuilder().addPathSegment(method).build(); + return getRequestBuilder().get().url(url).build(); + } + + RequestBody getJsonRequestBody(Object object) { + return RequestBody.create(JSON, mapToJson(object)); + } + + void addQueryParameters(Builder urlBuilder, Object object) { + Map queryMap = MAPPER.convertValue(object, new TypeReference>() {}); + queryMap.keySet().forEach(key -> { + Object type = queryMap.get(key); + if (!(type instanceof String)) { + type = mapToJson(type); + } + urlBuilder.addQueryParameter(key, (String) type); + }); + } +} diff --git a/src/main/java/org/arvados/client/api/client/CollectionsApiClient.java b/src/main/java/org/arvados/client/api/client/CollectionsApiClient.java new file mode 100644 index 0000000000..141f02deba --- /dev/null +++ b/src/main/java/org/arvados/client/api/client/CollectionsApiClient.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import org.arvados.client.api.model.Collection; +import org.arvados.client.api.model.CollectionList; +import org.arvados.client.config.ConfigProvider; +import org.slf4j.Logger; + +public class CollectionsApiClient extends BaseStandardApiClient { + + private static final String RESOURCE = "collections"; + private final Logger log = org.slf4j.LoggerFactory.getLogger(CollectionsApiClient.class); + + public CollectionsApiClient(ConfigProvider config) { + super(config); + } + + @Override + public Collection create(Collection type) { + Collection newCollection = super.create(type); + log.debug(String.format("New collection '%s' with UUID %s has been created", newCollection.getName(), newCollection.getUuid())); + return newCollection; + } + + @Override + String getResource() { + return RESOURCE; + } + + @Override + Class getType() { + return Collection.class; + } + + @Override + Class getListType() { + return CollectionList.class; + } +} diff --git a/src/main/java/org/arvados/client/api/client/CountingFileRequestBody.java b/src/main/java/org/arvados/client/api/client/CountingFileRequestBody.java new file mode 100644 index 0000000000..43fcdba5c6 --- /dev/null +++ b/src/main/java/org/arvados/client/api/client/CountingFileRequestBody.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.BufferedSink; +import okio.Okio; +import okio.Source; +import org.slf4j.Logger; + +import java.io.File; + +/** + * Based on: + * {@link} https://gist.github.com/eduardb/dd2dc530afd37108e1ac + */ +public class CountingFileRequestBody extends RequestBody { + + private static final int SEGMENT_SIZE = 2048; // okio.Segment.SIZE + private static final MediaType CONTENT_BINARY = MediaType.parse(com.google.common.net.MediaType.OCTET_STREAM.toString()); + + private final File file; + private final ProgressListener listener; + + CountingFileRequestBody(final File file, final ProgressListener listener) { + this.file = file; + this.listener = listener; + } + + @Override + public long contentLength() { + return file.length(); + } + + @Override + public MediaType contentType() { + return CONTENT_BINARY; + } + + @Override + public void writeTo(BufferedSink sink) { + try (Source source = Okio.source(file)) { + long total = 0; + long read; + + while ((read = source.read(sink.buffer(), SEGMENT_SIZE)) != -1) { + total += read; + sink.flush(); + listener.updateProgress(total); + + } + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + //ignore + } + } + + static class TransferData { + + private final Logger log = org.slf4j.LoggerFactory.getLogger(TransferData.class); + private int progressValue; + private long totalSize; + + TransferData(long totalSize) { + this.progressValue = 0; + this.totalSize = totalSize; + } + + void updateTransferProgress(long transferred) { + float progress = (transferred / (float) totalSize) * 100; + if (progressValue != (int) progress) { + progressValue = (int) progress; + log.debug("{} / {} / {}%", transferred, totalSize, progressValue); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/arvados/client/api/client/GroupsApiClient.java b/src/main/java/org/arvados/client/api/client/GroupsApiClient.java new file mode 100644 index 0000000000..75aa9ca309 --- /dev/null +++ b/src/main/java/org/arvados/client/api/client/GroupsApiClient.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import okhttp3.HttpUrl; +import okhttp3.HttpUrl.Builder; +import okhttp3.Request; +import okhttp3.RequestBody; +import org.arvados.client.api.model.Group; +import org.arvados.client.api.model.GroupList; +import org.arvados.client.api.model.argument.ContentsGroup; +import org.arvados.client.api.model.argument.UntrashGroup; +import org.arvados.client.config.ConfigProvider; +import org.slf4j.Logger; + +public class GroupsApiClient extends BaseStandardApiClient { + + private static final String RESOURCE = "groups"; + private final Logger log = org.slf4j.LoggerFactory.getLogger(GroupsApiClient.class); + + public GroupsApiClient(ConfigProvider config) { + super(config); + } + + public GroupList contents(ContentsGroup contentsGroup) { + log.debug("Get {} contents", getType().getSimpleName()); + Builder urlBuilder = getUrlBuilder().addPathSegment("contents"); + addQueryParameters(urlBuilder, contentsGroup); + HttpUrl url = urlBuilder.build(); + Request request = getRequestBuilder().url(url).build(); + return callForList(request); + } + + public Group untrash(UntrashGroup untrashGroup) { + log.debug("Untrash {} by UUID {}", getType().getSimpleName(), untrashGroup.getUuid()); + HttpUrl url = getUrlBuilder().addPathSegment(untrashGroup.getUuid()).addPathSegment("untrash").build(); + RequestBody requestBody = getJsonRequestBody(untrashGroup); + Request request = getRequestBuilder().post(requestBody).url(url).build(); + return callForType(request); + } + + @Override + String getResource() { + return RESOURCE; + } + + @Override + Class getType() { + return Group.class; + } + + @Override + Class getListType() { + return GroupList.class; + } +} diff --git a/src/main/java/org/arvados/client/api/client/KeepServerApiClient.java b/src/main/java/org/arvados/client/api/client/KeepServerApiClient.java new file mode 100644 index 0000000000..a9306ca2ec --- /dev/null +++ b/src/main/java/org/arvados/client/api/client/KeepServerApiClient.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import okhttp3.Request; +import okhttp3.RequestBody; +import org.arvados.client.api.client.CountingFileRequestBody.TransferData; +import org.arvados.client.common.Headers; +import org.arvados.client.config.ConfigProvider; +import org.slf4j.Logger; + +import java.io.File; +import java.util.Map; + +public class KeepServerApiClient extends BaseApiClient { + + private final Logger log = org.slf4j.LoggerFactory.getLogger(KeepServerApiClient.class); + + public KeepServerApiClient(ConfigProvider config) { + super(config); + } + + public String upload(String url, Map headers, File body) { + + log.debug("Upload file {} to server location {}", body, url); + + final TransferData transferData = new TransferData(body.length()); + + RequestBody requestBody = new CountingFileRequestBody(body, transferData::updateTransferProgress); + + Request request = getRequestBuilder() + .url(url) + .addHeader(Headers.X_KEEP_DESIRED_REPLICAS, headers.get(Headers.X_KEEP_DESIRED_REPLICAS)) + .put(requestBody) + .build(); + + return newCall(request); + } + + public byte[] download(String url) { + + Request request = getRequestBuilder() + .url(url) + .get() + .build(); + + return newFileCall(request); + } +} diff --git a/src/main/java/org/arvados/client/api/client/KeepServicesApiClient.java b/src/main/java/org/arvados/client/api/client/KeepServicesApiClient.java new file mode 100644 index 0000000000..81a9d6f5da --- /dev/null +++ b/src/main/java/org/arvados/client/api/client/KeepServicesApiClient.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import org.arvados.client.api.model.KeepService; +import org.arvados.client.api.model.KeepServiceList; +import org.arvados.client.config.ConfigProvider; +import org.slf4j.Logger; + +public class KeepServicesApiClient extends BaseStandardApiClient { + + private static final String RESOURCE = "keep_services"; + private final Logger log = org.slf4j.LoggerFactory.getLogger(KeepServicesApiClient.class); + + public KeepServicesApiClient(ConfigProvider config) { + super(config); + } + + public KeepServiceList accessible() { + log.debug("Get list of accessible {}", getType().getSimpleName()); + return callForList(getNoArgumentMethodRequest("accessible")); + } + + @Override + String getResource() { + return RESOURCE; + } + + @Override + Class getType() { + return KeepService.class; + } + + @Override + Class getListType() { + return KeepServiceList.class; + } +} diff --git a/src/main/java/org/arvados/client/api/client/KeepWebApiClient.java b/src/main/java/org/arvados/client/api/client/KeepWebApiClient.java new file mode 100644 index 0000000000..4cd08b7832 --- /dev/null +++ b/src/main/java/org/arvados/client/api/client/KeepWebApiClient.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import okhttp3.HttpUrl; +import okhttp3.Request; +import org.arvados.client.config.ConfigProvider; + +public class KeepWebApiClient extends BaseApiClient { + + public KeepWebApiClient(ConfigProvider config) { + super(config); + } + + public byte[] download(String collectionUuid, String filePathName) { + Request request = getRequestBuilder() + .url(getUrlBuilder(collectionUuid,filePathName).build()) + .get() + .build(); + + return newFileCall(request); + } + + private HttpUrl.Builder getUrlBuilder(String collectionUuid, String filePathName) { + return new HttpUrl.Builder() + .scheme(config.getApiProtocol()) + .host(config.getKeepWebHost()) + .port(config.getKeepWebPort()) + .addPathSegment("c=" + collectionUuid) + .addPathSegment(filePathName); + } +} diff --git a/src/main/java/org/arvados/client/api/client/ProgressListener.java b/src/main/java/org/arvados/client/api/client/ProgressListener.java new file mode 100644 index 0000000000..8563adcc76 --- /dev/null +++ b/src/main/java/org/arvados/client/api/client/ProgressListener.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +@FunctionalInterface +public interface ProgressListener { + + void updateProgress(long num); +} \ No newline at end of file diff --git a/src/main/java/org/arvados/client/api/client/UsersApiClient.java b/src/main/java/org/arvados/client/api/client/UsersApiClient.java new file mode 100644 index 0000000000..5bf1d07458 --- /dev/null +++ b/src/main/java/org/arvados/client/api/client/UsersApiClient.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import okhttp3.Request; +import org.arvados.client.api.model.User; +import org.arvados.client.api.model.UserList; +import org.arvados.client.config.ConfigProvider; +import org.slf4j.Logger; + +public class UsersApiClient extends BaseStandardApiClient { + + private static final String RESOURCE = "users"; + private final Logger log = org.slf4j.LoggerFactory.getLogger(UsersApiClient.class); + + public UsersApiClient(ConfigProvider config) { + super(config); + } + + public User current() { + log.debug("Get current {}", getType().getSimpleName()); + Request request = getNoArgumentMethodRequest("current"); + return callForType(request); + } + + public User system() { + log.debug("Get system {}", getType().getSimpleName()); + Request request = getNoArgumentMethodRequest("system"); + return callForType(request); + } + + @Override + String getResource() { + return RESOURCE; + } + + @Override + Class getType() { + return User.class; + } + + @Override + Class getListType() { + return UserList.class; + } +} diff --git a/src/main/java/org/arvados/client/api/client/factory/OkHttpClientFactory.java b/src/main/java/org/arvados/client/api/client/factory/OkHttpClientFactory.java new file mode 100644 index 0000000000..0e95e661e7 --- /dev/null +++ b/src/main/java/org/arvados/client/api/client/factory/OkHttpClientFactory.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client.factory; + +import okhttp3.OkHttpClient; +import org.arvados.client.exception.ArvadosClientException; +import org.slf4j.Logger; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +public class OkHttpClientFactory { + + private final Logger log = org.slf4j.LoggerFactory.getLogger(OkHttpClientFactory.class); + + OkHttpClientFactory() { + } + + public static OkHttpClientFactoryBuilder builder() { + return new OkHttpClientFactoryBuilder(); + } + + public OkHttpClient create(boolean apiHostInsecure) { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (apiHostInsecure) { + trustAllCertificates(builder); + } + return builder.build(); + } + + private void trustAllCertificates(OkHttpClient.Builder builder) { + log.warn("Creating unsafe OkHttpClient. All SSL certificates will be accepted."); + try { + // Create a trust manager that does not validate certificate chains + final TrustManager[] trustAllCerts = new TrustManager[] { createX509TrustManager() }; + + // Install the all-trusting trust manager + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new SecureRandom()); + // Create an ssl socket factory with our all-trusting manager + final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]); + builder.hostnameVerifier((hostname, session) -> true); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new ArvadosClientException("Error establishing SSL context", e); + } + } + + private static X509TrustManager createX509TrustManager() { + return new X509TrustManager() { + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[] {}; + } + }; + } + + public static class OkHttpClientFactoryBuilder { + OkHttpClientFactoryBuilder() { + } + + public OkHttpClientFactory build() { + return new OkHttpClientFactory(); + } + + public String toString() { + return "OkHttpClientFactory.OkHttpClientFactoryBuilder()"; + } + } +} diff --git a/src/main/java/org/arvados/client/api/model/ApiError.java b/src/main/java/org/arvados/client/api/model/ApiError.java new file mode 100644 index 0000000000..1529f9c30c --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/ApiError.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "errors", "error_token" }) +public class ApiError { + + @JsonProperty("errors") + private List errors; + @JsonProperty("error_token") + private String errorToken; + + public List getErrors() { + return this.errors; + } + + public String getErrorToken() { + return this.errorToken; + } + + public void setErrors(List errors) { + this.errors = errors; + } + + public void setErrorToken(String errorToken) { + this.errorToken = errorToken; + } +} diff --git a/src/main/java/org/arvados/client/api/model/Collection.java b/src/main/java/org/arvados/client/api/model/Collection.java new file mode 100644 index 0000000000..b1652e2a3b --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/Collection.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.time.LocalDateTime; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "portable_data_hash", "replication_desired", "replication_confirmed_at", "replication_confirmed", "manifest_text", + "name", "description", "properties", "delete_at", "trash_at", "is_trashed" }) +public class Collection extends Item { + + @JsonProperty("portable_data_hash") + private String portableDataHash; + @JsonProperty("replication_desired") + private Integer replicationDesired; + @JsonProperty("replication_confirmed_at") + private LocalDateTime replicationConfirmedAt; + @JsonProperty("replication_confirmed") + private Integer replicationConfirmed; + @JsonProperty("manifest_text") + private String manifestText; + @JsonProperty("name") + private String name; + @JsonProperty("description") + private String description; + @JsonProperty("properties") + private Object properties; + @JsonProperty("delete_at") + private LocalDateTime deleteAt; + @JsonProperty("trash_at") + private LocalDateTime trashAt; + @JsonProperty("is_trashed") + private Boolean trashed; + + public String getPortableDataHash() { + return this.portableDataHash; + } + + public Integer getReplicationDesired() { + return this.replicationDesired; + } + + public LocalDateTime getReplicationConfirmedAt() { + return this.replicationConfirmedAt; + } + + public Integer getReplicationConfirmed() { + return this.replicationConfirmed; + } + + public String getManifestText() { + return this.manifestText; + } + + public String getName() { + return this.name; + } + + public String getDescription() { + return this.description; + } + + public Object getProperties() { + return this.properties; + } + + public LocalDateTime getDeleteAt() { + return this.deleteAt; + } + + public LocalDateTime getTrashAt() { + return this.trashAt; + } + + public Boolean getTrashed() { + return this.trashed; + } + + public void setPortableDataHash(String portableDataHash) { + this.portableDataHash = portableDataHash; + } + + public void setReplicationDesired(Integer replicationDesired) { + this.replicationDesired = replicationDesired; + } + + public void setReplicationConfirmedAt(LocalDateTime replicationConfirmedAt) { + this.replicationConfirmedAt = replicationConfirmedAt; + } + + public void setReplicationConfirmed(Integer replicationConfirmed) { + this.replicationConfirmed = replicationConfirmed; + } + + public void setManifestText(String manifestText) { + this.manifestText = manifestText; + } + + public void setName(String name) { + this.name = name; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setProperties(Object properties) { + this.properties = properties; + } + + public void setDeleteAt(LocalDateTime deleteAt) { + this.deleteAt = deleteAt; + } + + public void setTrashAt(LocalDateTime trashAt) { + this.trashAt = trashAt; + } + + public void setTrashed(Boolean trashed) { + this.trashed = trashed; + } + + public String toString() { + return "Collection(portableDataHash=" + this.getPortableDataHash() + ", replicationDesired=" + this.getReplicationDesired() + ", replicationConfirmedAt=" + this.getReplicationConfirmedAt() + ", replicationConfirmed=" + this.getReplicationConfirmed() + ", manifestText=" + this.getManifestText() + ", name=" + this.getName() + ", description=" + this.getDescription() + ", properties=" + this.getProperties() + ", deleteAt=" + this.getDeleteAt() + ", trashAt=" + this.getTrashAt() + ", trashed=" + this.getTrashed() + ")"; + } +} \ No newline at end of file diff --git a/src/main/java/org/arvados/client/api/model/CollectionList.java b/src/main/java/org/arvados/client/api/model/CollectionList.java new file mode 100644 index 0000000000..4dae7f630c --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/CollectionList.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "items" }) +public class CollectionList extends ItemList { + + @JsonProperty("items") + private List items; + + public List getItems() { + return this.items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/src/main/java/org/arvados/client/api/model/Group.java b/src/main/java/org/arvados/client/api/model/Group.java new file mode 100644 index 0000000000..e9fbdb744d --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/Group.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.time.LocalDateTime; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "command", "container_count", "container_count_max", "container_image", "container_uuid", "cwd", "environment", "expires_at", + "filters", "log_uuid", "mounts", "output_name", "output_path", "output_uuid", "output_ttl", "priority", "properties", "requesting_container_uuid", + "runtime_constraints", "scheduling_parameters", "state", "use_existing" }) +public class Group extends Item { + + @JsonProperty("name") + private String name; + @JsonProperty("group_class") + private String groupClass; + @JsonProperty("description") + private String description; + @JsonProperty("writable_by") + private List writableBy; + @JsonProperty("delete_at") + private LocalDateTime deleteAt; + @JsonProperty("trash_at") + private LocalDateTime trashAt; + @JsonProperty("is_trashed") + private Boolean isTrashed; + @JsonProperty("command") + private List command; + @JsonProperty("container_count") + private Integer containerCount; + @JsonProperty("container_count_max") + private Integer containerCountMax; + @JsonProperty("container_image") + private String containerImage; + @JsonProperty("container_uuid") + private String containerUuid; + @JsonProperty("cwd") + private String cwd; + @JsonProperty("environment") + private Object environment; + @JsonProperty("expires_at") + private LocalDateTime expiresAt; + @JsonProperty("filters") + private List filters; + @JsonProperty("log_uuid") + private String logUuid; + @JsonProperty("mounts") + private Object mounts; + @JsonProperty("output_name") + private String outputName; + @JsonProperty("output_path") + private String outputPath; + @JsonProperty("output_uuid") + private String outputUuid; + @JsonProperty("output_ttl") + private Integer outputTtl; + @JsonProperty("priority") + private Integer priority; + @JsonProperty("properties") + private Object properties; + @JsonProperty("requesting_container_uuid") + private String requestingContainerUuid; + @JsonProperty("runtime_constraints") + private RuntimeConstraints runtimeConstraints; + @JsonProperty("scheduling_parameters") + private Object schedulingParameters; + @JsonProperty("state") + private String state; + @JsonProperty("use_existing") + private Boolean useExisting; + + public String getName() { + return this.name; + } + + public String getGroupClass() { + return this.groupClass; + } + + public String getDescription() { + return this.description; + } + + public List getWritableBy() { + return this.writableBy; + } + + public LocalDateTime getDeleteAt() { + return this.deleteAt; + } + + public LocalDateTime getTrashAt() { + return this.trashAt; + } + + public Boolean getIsTrashed() { + return this.isTrashed; + } + + public List getCommand() { + return this.command; + } + + public Integer getContainerCount() { + return this.containerCount; + } + + public Integer getContainerCountMax() { + return this.containerCountMax; + } + + public String getContainerImage() { + return this.containerImage; + } + + public String getContainerUuid() { + return this.containerUuid; + } + + public String getCwd() { + return this.cwd; + } + + public Object getEnvironment() { + return this.environment; + } + + public LocalDateTime getExpiresAt() { + return this.expiresAt; + } + + public List getFilters() { + return this.filters; + } + + public String getLogUuid() { + return this.logUuid; + } + + public Object getMounts() { + return this.mounts; + } + + public String getOutputName() { + return this.outputName; + } + + public String getOutputPath() { + return this.outputPath; + } + + public String getOutputUuid() { + return this.outputUuid; + } + + public Integer getOutputTtl() { + return this.outputTtl; + } + + public Integer getPriority() { + return this.priority; + } + + public Object getProperties() { + return this.properties; + } + + public String getRequestingContainerUuid() { + return this.requestingContainerUuid; + } + + public RuntimeConstraints getRuntimeConstraints() { + return this.runtimeConstraints; + } + + public Object getSchedulingParameters() { + return this.schedulingParameters; + } + + public String getState() { + return this.state; + } + + public Boolean getUseExisting() { + return this.useExisting; + } + + public void setName(String name) { + this.name = name; + } + + public void setGroupClass(String groupClass) { + this.groupClass = groupClass; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setWritableBy(List writableBy) { + this.writableBy = writableBy; + } + + public void setDeleteAt(LocalDateTime deleteAt) { + this.deleteAt = deleteAt; + } + + public void setTrashAt(LocalDateTime trashAt) { + this.trashAt = trashAt; + } + + public void setIsTrashed(Boolean isTrashed) { + this.isTrashed = isTrashed; + } + + public void setCommand(List command) { + this.command = command; + } + + public void setContainerCount(Integer containerCount) { + this.containerCount = containerCount; + } + + public void setContainerCountMax(Integer containerCountMax) { + this.containerCountMax = containerCountMax; + } + + public void setContainerImage(String containerImage) { + this.containerImage = containerImage; + } + + public void setContainerUuid(String containerUuid) { + this.containerUuid = containerUuid; + } + + public void setCwd(String cwd) { + this.cwd = cwd; + } + + public void setEnvironment(Object environment) { + this.environment = environment; + } + + public void setExpiresAt(LocalDateTime expiresAt) { + this.expiresAt = expiresAt; + } + + public void setFilters(List filters) { + this.filters = filters; + } + + public void setLogUuid(String logUuid) { + this.logUuid = logUuid; + } + + public void setMounts(Object mounts) { + this.mounts = mounts; + } + + public void setOutputName(String outputName) { + this.outputName = outputName; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public void setOutputUuid(String outputUuid) { + this.outputUuid = outputUuid; + } + + public void setOutputTtl(Integer outputTtl) { + this.outputTtl = outputTtl; + } + + public void setPriority(Integer priority) { + this.priority = priority; + } + + public void setProperties(Object properties) { + this.properties = properties; + } + + public void setRequestingContainerUuid(String requestingContainerUuid) { + this.requestingContainerUuid = requestingContainerUuid; + } + + public void setRuntimeConstraints(RuntimeConstraints runtimeConstraints) { + this.runtimeConstraints = runtimeConstraints; + } + + public void setSchedulingParameters(Object schedulingParameters) { + this.schedulingParameters = schedulingParameters; + } + + public void setState(String state) { + this.state = state; + } + + public void setUseExisting(Boolean useExisting) { + this.useExisting = useExisting; + } + + public String toString() { + return "Group(name=" + this.getName() + ", groupClass=" + this.getGroupClass() + ", description=" + this.getDescription() + ", writableBy=" + this.getWritableBy() + ", deleteAt=" + this.getDeleteAt() + ", trashAt=" + this.getTrashAt() + ", isTrashed=" + this.getIsTrashed() + ", command=" + this.getCommand() + ", containerCount=" + this.getContainerCount() + ", containerCountMax=" + this.getContainerCountMax() + ", containerImage=" + this.getContainerImage() + ", containerUuid=" + this.getContainerUuid() + ", cwd=" + this.getCwd() + ", environment=" + this.getEnvironment() + ", expiresAt=" + this.getExpiresAt() + ", filters=" + this.getFilters() + ", logUuid=" + this.getLogUuid() + ", mounts=" + this.getMounts() + ", outputName=" + this.getOutputName() + ", outputPath=" + this.getOutputPath() + ", outputUuid=" + this.getOutputUuid() + ", outputTtl=" + this.getOutputTtl() + ", priority=" + this.getPriority() + ", properties=" + this.getProperties() + ", requestingContainerUuid=" + this.getRequestingContainerUuid() + ", runtimeConstraints=" + this.getRuntimeConstraints() + ", schedulingParameters=" + this.getSchedulingParameters() + ", state=" + this.getState() + ", useExisting=" + this.getUseExisting() + ")"; + } +} diff --git a/src/main/java/org/arvados/client/api/model/GroupList.java b/src/main/java/org/arvados/client/api/model/GroupList.java new file mode 100644 index 0000000000..c78d8ff145 --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/GroupList.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "items" }) +public class GroupList extends ItemList { + + @JsonProperty("items") + private List items; + + public List getItems() { + return this.items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/src/main/java/org/arvados/client/api/model/Item.java b/src/main/java/org/arvados/client/api/model/Item.java new file mode 100644 index 0000000000..be30e57843 --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/Item.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.time.LocalDateTime; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "href", "kind", "etag", "uuid", "owner_uuid", "created_at", "modified_by_client_uuid", + "modified_by_user_uuid", "modified_at", "updated_at" }) +public abstract class Item { + + @JsonProperty("href") + private String href; + @JsonProperty("kind") + private String kind; + @JsonProperty("etag") + private String etag; + @JsonProperty("uuid") + private String uuid; + @JsonProperty("owner_uuid") + private String ownerUuid; + @JsonProperty("created_at") + private LocalDateTime createdAt; + @JsonProperty("modified_by_client_uuid") + private String modifiedByClientUuid; + @JsonProperty("modified_by_user_uuid") + private String modifiedByUserUuid; + @JsonProperty("modified_at") + private LocalDateTime modifiedAt; + @JsonProperty("updated_at") + private LocalDateTime updatedAt; + + public String getHref() { + return this.href; + } + + public String getKind() { + return this.kind; + } + + public String getEtag() { + return this.etag; + } + + public String getUuid() { + return this.uuid; + } + + public String getOwnerUuid() { + return this.ownerUuid; + } + + public LocalDateTime getCreatedAt() { + return this.createdAt; + } + + public String getModifiedByClientUuid() { + return this.modifiedByClientUuid; + } + + public String getModifiedByUserUuid() { + return this.modifiedByUserUuid; + } + + public LocalDateTime getModifiedAt() { + return this.modifiedAt; + } + + public LocalDateTime getUpdatedAt() { + return this.updatedAt; + } + + public void setHref(String href) { + this.href = href; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public void setEtag(String etag) { + this.etag = etag; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public void setOwnerUuid(String ownerUuid) { + this.ownerUuid = ownerUuid; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public void setModifiedByClientUuid(String modifiedByClientUuid) { + this.modifiedByClientUuid = modifiedByClientUuid; + } + + public void setModifiedByUserUuid(String modifiedByUserUuid) { + this.modifiedByUserUuid = modifiedByUserUuid; + } + + public void setModifiedAt(LocalDateTime modifiedAt) { + this.modifiedAt = modifiedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/org/arvados/client/api/model/ItemList.java b/src/main/java/org/arvados/client/api/model/ItemList.java new file mode 100644 index 0000000000..b15a3628f2 --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/ItemList.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "kind", "etag", "self_link", "offset", "limit", "items_available" }) +public class ItemList { + + @JsonProperty("kind") + private String kind; + @JsonProperty("etag") + private String etag; + @JsonProperty("self_link") + private String selfLink; + @JsonProperty("offset") + private Object offset; + @JsonProperty("limit") + private Object limit; + @JsonProperty("items_available") + private Integer itemsAvailable; + + public String getKind() { + return this.kind; + } + + public String getEtag() { + return this.etag; + } + + public String getSelfLink() { + return this.selfLink; + } + + public Object getOffset() { + return this.offset; + } + + public Object getLimit() { + return this.limit; + } + + public Integer getItemsAvailable() { + return this.itemsAvailable; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public void setEtag(String etag) { + this.etag = etag; + } + + public void setSelfLink(String selfLink) { + this.selfLink = selfLink; + } + + public void setOffset(Object offset) { + this.offset = offset; + } + + public void setLimit(Object limit) { + this.limit = limit; + } + + public void setItemsAvailable(Integer itemsAvailable) { + this.itemsAvailable = itemsAvailable; + } +} \ No newline at end of file diff --git a/src/main/java/org/arvados/client/api/model/KeepService.java b/src/main/java/org/arvados/client/api/model/KeepService.java new file mode 100644 index 0000000000..c29b44cb67 --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/KeepService.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.*; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "service_host", "service_port", "service_ssl_flag", "service_type", "read_only" }) +public class KeepService extends Item { + + @JsonProperty("service_host") + private String serviceHost; + @JsonProperty("service_port") + private Integer servicePort; + @JsonProperty("service_ssl_flag") + private Boolean serviceSslFlag; + @JsonProperty("service_type") + private String serviceType; + @JsonProperty("read_only") + private Boolean readOnly; + @JsonIgnore + private String serviceRoot; + + public String getServiceHost() { + return this.serviceHost; + } + + public Integer getServicePort() { + return this.servicePort; + } + + public Boolean getServiceSslFlag() { + return this.serviceSslFlag; + } + + public String getServiceType() { + return this.serviceType; + } + + public Boolean getReadOnly() { + return this.readOnly; + } + + public String getServiceRoot() { + return this.serviceRoot; + } + + public void setServiceHost(String serviceHost) { + this.serviceHost = serviceHost; + } + + public void setServicePort(Integer servicePort) { + this.servicePort = servicePort; + } + + public void setServiceSslFlag(Boolean serviceSslFlag) { + this.serviceSslFlag = serviceSslFlag; + } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } + + public void setReadOnly(Boolean readOnly) { + this.readOnly = readOnly; + } + + public void setServiceRoot(String serviceRoot) { + this.serviceRoot = serviceRoot; + } +} \ No newline at end of file diff --git a/src/main/java/org/arvados/client/api/model/KeepServiceList.java b/src/main/java/org/arvados/client/api/model/KeepServiceList.java new file mode 100644 index 0000000000..bbc09dc289 --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/KeepServiceList.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "items" }) +public class KeepServiceList extends ItemList { + + @JsonProperty("items") + private List items; + + public List getItems() { + return this.items; + } + + public void setItems(List items) { + this.items = items; + } +} \ No newline at end of file diff --git a/src/main/java/org/arvados/client/api/model/RuntimeConstraints.java b/src/main/java/org/arvados/client/api/model/RuntimeConstraints.java new file mode 100644 index 0000000000..a23cd98eb4 --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/RuntimeConstraints.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "API", "vcpus", "ram", "keep_cache_ram" }) +public class RuntimeConstraints { + + @JsonProperty("API") + private Boolean api; + @JsonProperty("vcpus") + private Integer vcpus; + @JsonProperty("ram") + private Long ram; + @JsonProperty("keep_cache_ram") + private Long keepCacheRam; + + public Boolean getApi() { + return this.api; + } + + public Integer getVcpus() { + return this.vcpus; + } + + public Long getRam() { + return this.ram; + } + + public Long getKeepCacheRam() { + return this.keepCacheRam; + } + + public void setApi(Boolean api) { + this.api = api; + } + + public void setVcpus(Integer vcpus) { + this.vcpus = vcpus; + } + + public void setRam(Long ram) { + this.ram = ram; + } + + public void setKeepCacheRam(Long keepCacheRam) { + this.keepCacheRam = keepCacheRam; + } +} diff --git a/src/main/java/org/arvados/client/api/model/User.java b/src/main/java/org/arvados/client/api/model/User.java new file mode 100644 index 0000000000..5c86a07bdf --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/User.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "email", "username", "full_name", "first_name", "last_name", "identity_url", "is_active", "is_admin", "is_invited", + "prefs", "writable_by", "default_owner_uuid" }) +public class User extends Item { + + @JsonProperty("email") + private String email; + @JsonProperty("username") + private String username; + @JsonProperty("full_name") + private String fullName; + @JsonProperty("first_name") + private String firstName; + @JsonProperty("last_name") + private String lastName; + @JsonProperty("identity_url") + private String identityUrl; + @JsonProperty("is_active") + private Boolean isActive; + @JsonProperty("is_admin") + private Boolean isAdmin; + @JsonProperty("is_invited") + private Boolean isInvited; + @JsonProperty("prefs") + private Object prefs; + @JsonProperty("writable_by") + private List writableBy; + @JsonProperty("default_owner_uuid") + private Boolean defaultOwnerUuid; + + public String getEmail() { + return this.email; + } + + public String getUsername() { + return this.username; + } + + public String getFullName() { + return this.fullName; + } + + public String getFirstName() { + return this.firstName; + } + + public String getLastName() { + return this.lastName; + } + + public String getIdentityUrl() { + return this.identityUrl; + } + + public Boolean getIsActive() { + return this.isActive; + } + + public Boolean getIsAdmin() { + return this.isAdmin; + } + + public Boolean getIsInvited() { + return this.isInvited; + } + + public Object getPrefs() { + return this.prefs; + } + + public List getWritableBy() { + return this.writableBy; + } + + public Boolean getDefaultOwnerUuid() { + return this.defaultOwnerUuid; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public void setIdentityUrl(String identityUrl) { + this.identityUrl = identityUrl; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } + + public void setIsAdmin(Boolean isAdmin) { + this.isAdmin = isAdmin; + } + + public void setIsInvited(Boolean isInvited) { + this.isInvited = isInvited; + } + + public void setPrefs(Object prefs) { + this.prefs = prefs; + } + + public void setWritableBy(List writableBy) { + this.writableBy = writableBy; + } + + public void setDefaultOwnerUuid(Boolean defaultOwnerUuid) { + this.defaultOwnerUuid = defaultOwnerUuid; + } + + public String toString() { + return "User(email=" + this.getEmail() + ", username=" + this.getUsername() + ", fullName=" + this.getFullName() + ", firstName=" + this.getFirstName() + ", lastName=" + this.getLastName() + ", identityUrl=" + this.getIdentityUrl() + ", isActive=" + this.getIsActive() + ", isAdmin=" + this.getIsAdmin() + ", isInvited=" + this.getIsInvited() + ", prefs=" + this.getPrefs() + ", writableBy=" + this.getWritableBy() + ", defaultOwnerUuid=" + this.getDefaultOwnerUuid() + ")"; + } +} diff --git a/src/main/java/org/arvados/client/api/model/UserList.java b/src/main/java/org/arvados/client/api/model/UserList.java new file mode 100644 index 0000000000..e148e72662 --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/UserList.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonPropertyOrder({ "items" }) +public class UserList extends ItemList { + + @JsonProperty("items") + private List items; + + public List getItems() { + return this.items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/src/main/java/org/arvados/client/api/model/argument/Argument.java b/src/main/java/org/arvados/client/api/model/argument/Argument.java new file mode 100644 index 0000000000..6da44088c2 --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/argument/Argument.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model.argument; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public abstract class Argument { + + @JsonIgnore + private String uuid; + + public String getUuid() { + return this.uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } +} diff --git a/src/main/java/org/arvados/client/api/model/argument/ContentsGroup.java b/src/main/java/org/arvados/client/api/model/argument/ContentsGroup.java new file mode 100644 index 0000000000..16febf784c --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/argument/ContentsGroup.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model.argument; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ "limit", "order", "filters", "recursive" }) +public class ContentsGroup extends Argument { + + @JsonProperty("limit") + private Integer limit; + + @JsonProperty("order") + private String order; + + @JsonProperty("filters") + private List filters; + + @JsonProperty("recursive") + private Boolean recursive; + + public Integer getLimit() { + return this.limit; + } + + public String getOrder() { + return this.order; + } + + public List getFilters() { + return this.filters; + } + + public Boolean getRecursive() { + return this.recursive; + } + + public void setLimit(Integer limit) { + this.limit = limit; + } + + public void setOrder(String order) { + this.order = order; + } + + public void setFilters(List filters) { + this.filters = filters; + } + + public void setRecursive(Boolean recursive) { + this.recursive = recursive; + } +} diff --git a/src/main/java/org/arvados/client/api/model/argument/Filter.java b/src/main/java/org/arvados/client/api/model/argument/Filter.java new file mode 100644 index 0000000000..ae16dec4ed --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/argument/Filter.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model.argument; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonFormat(shape = JsonFormat.Shape.ARRAY) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ "attribute", "operator", "operand" }) +public class Filter { + + @JsonProperty("attribute") + private String attribute; + + @JsonProperty("operator") + private Operator operator; + + @JsonProperty("operand") + private Object operand; + + private Filter(String attribute, Operator operator, Object operand) { + this.attribute = attribute; + this.operator = operator; + this.operand = operand; + } + + public static Filter of(String attribute, Operator operator, Object operand) { + return new Filter(attribute, operator, operand); + } + + public String getAttribute() { + return this.attribute; + } + + public Operator getOperator() { + return this.operator; + } + + public Object getOperand() { + return this.operand; + } + + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof Filter)) return false; + final Filter other = (Filter) o; + final Object this$attribute = this.getAttribute(); + final Object other$attribute = other.getAttribute(); + if (this$attribute == null ? other$attribute != null : !this$attribute.equals(other$attribute)) return false; + final Object this$operator = this.getOperator(); + final Object other$operator = other.getOperator(); + if (this$operator == null ? other$operator != null : !this$operator.equals(other$operator)) return false; + final Object this$operand = this.getOperand(); + final Object other$operand = other.getOperand(); + if (this$operand == null ? other$operand != null : !this$operand.equals(other$operand)) return false; + return true; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $attribute = this.getAttribute(); + result = result * PRIME + ($attribute == null ? 43 : $attribute.hashCode()); + final Object $operator = this.getOperator(); + result = result * PRIME + ($operator == null ? 43 : $operator.hashCode()); + final Object $operand = this.getOperand(); + result = result * PRIME + ($operand == null ? 43 : $operand.hashCode()); + return result; + } + + public String toString() { + return "Filter(attribute=" + this.getAttribute() + ", operator=" + this.getOperator() + ", operand=" + this.getOperand() + ")"; + } + + public enum Operator { + + @JsonProperty("<") + LESS, + + @JsonProperty("<=") + LESS_EQUALS, + + @JsonProperty(">=") + MORE_EQUALS, + + @JsonProperty(">") + MORE, + + @JsonProperty("like") + LIKE, + + @JsonProperty("ilike") + ILIKE, + + @JsonProperty("=") + EQUALS, + + @JsonProperty("!=") + NOT_EQUALS, + + @JsonProperty("in") + IN, + + @JsonProperty("not in") + NOT_IN, + + @JsonProperty("is_a") + IS_A + } +} diff --git a/src/main/java/org/arvados/client/api/model/argument/ListArgument.java b/src/main/java/org/arvados/client/api/model/argument/ListArgument.java new file mode 100644 index 0000000000..70231e6766 --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/argument/ListArgument.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model.argument; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ "limit", "offset", "filters", "order", "select", "distinct", "count" }) +public class ListArgument extends Argument { + + @JsonProperty("limit") + private Integer limit; + + @JsonProperty("offset") + private Integer offset; + + @JsonProperty("filters") + private List filters; + + @JsonProperty("order") + private List order; + + @JsonProperty("select") + private List select; + + @JsonProperty("distinct") + private Boolean distinct; + + @JsonProperty("count") + private Count count; + + + ListArgument(Integer limit, Integer offset, List filters, List order, List select, Boolean distinct, Count count) { + this.limit = limit; + this.offset = offset; + this.filters = filters; + this.order = order; + this.select = select; + this.distinct = distinct; + this.count = count; + } + + public static ListArgumentBuilder builder() { + return new ListArgumentBuilder(); + } + + public enum Count { + + @JsonProperty("exact") + EXACT, + + @JsonProperty("none") + NONE + } + + public static class ListArgumentBuilder { + private Integer limit; + private Integer offset; + private List filters; + private List order; + private List select; + private Boolean distinct; + private Count count; + + ListArgumentBuilder() { + } + + public ListArgumentBuilder limit(Integer limit) { + this.limit = limit; + return this; + } + + public ListArgumentBuilder offset(Integer offset) { + this.offset = offset; + return this; + } + + public ListArgumentBuilder filters(List filters) { + this.filters = filters; + return this; + } + + public ListArgumentBuilder order(List order) { + this.order = order; + return this; + } + + public ListArgumentBuilder select(List select) { + this.select = select; + return this; + } + + public ListArgumentBuilder distinct(Boolean distinct) { + this.distinct = distinct; + return this; + } + + public ListArgumentBuilder count(Count count) { + this.count = count; + return this; + } + + public ListArgument build() { + return new ListArgument(limit, offset, filters, order, select, distinct, count); + } + + public String toString() { + return "ListArgument.ListArgumentBuilder(limit=" + this.limit + + ", offset=" + this.offset + ", filters=" + this.filters + + ", order=" + this.order + ", select=" + this.select + + ", distinct=" + this.distinct + ", count=" + this.count + ")"; + } + } +} diff --git a/src/main/java/org/arvados/client/api/model/argument/UntrashGroup.java b/src/main/java/org/arvados/client/api/model/argument/UntrashGroup.java new file mode 100644 index 0000000000..027dbf7275 --- /dev/null +++ b/src/main/java/org/arvados/client/api/model/argument/UntrashGroup.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.model.argument; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ "ensure_unique_name" }) +public class UntrashGroup extends Argument { + + @JsonProperty("ensure_unique_name") + private Boolean ensureUniqueName; + + public Boolean getEnsureUniqueName() { + return this.ensureUniqueName; + } + + public void setEnsureUniqueName(Boolean ensureUniqueName) { + this.ensureUniqueName = ensureUniqueName; + } +} diff --git a/src/main/java/org/arvados/client/common/Characters.java b/src/main/java/org/arvados/client/common/Characters.java new file mode 100644 index 0000000000..1e49a71bcb --- /dev/null +++ b/src/main/java/org/arvados/client/common/Characters.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.common; + +public final class Characters { + + private Characters() {} + + public static final String SPACE = "\\040"; + public static final String NEW_LINE = "\n"; + public static final String SLASH = "/"; + public static final String DOT = "."; + public static final String COLON = ":"; + public static final String PERCENT = "%"; + public static final String QUOTE = "\""; +} diff --git a/src/main/java/org/arvados/client/common/Headers.java b/src/main/java/org/arvados/client/common/Headers.java new file mode 100644 index 0000000000..4b43ed9bb1 --- /dev/null +++ b/src/main/java/org/arvados/client/common/Headers.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.common; + +public final class Headers { + + private Headers() {} + + public static final String X_KEEP_DESIRED_REPLICAS = "X-Keep-Desired-Replicas"; +} diff --git a/src/main/java/org/arvados/client/common/Patterns.java b/src/main/java/org/arvados/client/common/Patterns.java new file mode 100644 index 0000000000..c852cb070c --- /dev/null +++ b/src/main/java/org/arvados/client/common/Patterns.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.common; + +public final class Patterns { + + public static final String HINT_PATTERN = "^[A-Z][A-Za-z0-9@_-]+$"; + public static final String FILE_TOKEN_PATTERN = "(\\d+:\\d+:\\S+)"; + public static final String LOCATOR_PATTERN = "([0-9a-f]{32})\\+([0-9]+)(\\+[A-Z][-A-Za-z0-9@_]*)*"; + public static final String GROUP_UUID_PATTERN = "[a-z0-9]{5}-j7d0g-[a-z0-9]{15}"; + public static final String USER_UUID_PATTERN = "[a-z0-9]{5}-tpzed-[a-z0-9]{15}"; + + private Patterns() {} +} diff --git a/src/main/java/org/arvados/client/config/ConfigProvider.java b/src/main/java/org/arvados/client/config/ConfigProvider.java new file mode 100644 index 0000000000..c9a4109313 --- /dev/null +++ b/src/main/java/org/arvados/client/config/ConfigProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.config; + +import java.io.File; + +public interface ConfigProvider { + + //API + boolean isApiHostInsecure(); + + String getKeepWebHost(); + + int getKeepWebPort(); + + String getApiHost(); + + int getApiPort(); + + String getApiToken(); + + String getApiProtocol(); + + + //FILE UPLOAD + int getFileSplitSize(); + + File getFileSplitDirectory(); + + int getNumberOfCopies(); + + int getNumberOfRetries(); + + +} diff --git a/src/main/java/org/arvados/client/config/ExternalConfigProvider.java b/src/main/java/org/arvados/client/config/ExternalConfigProvider.java new file mode 100644 index 0000000000..17e06966fa --- /dev/null +++ b/src/main/java/org/arvados/client/config/ExternalConfigProvider.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.config; + +import java.io.File; + +public class ExternalConfigProvider implements ConfigProvider { + + private boolean apiHostInsecure; + private String keepWebHost; + private int keepWebPort; + private String apiHost; + private int apiPort; + private String apiToken; + private String apiProtocol; + private int fileSplitSize; + private File fileSplitDirectory; + private int numberOfCopies; + private int numberOfRetries; + + ExternalConfigProvider(boolean apiHostInsecure, String keepWebHost, int keepWebPort, String apiHost, int apiPort, String apiToken, String apiProtocol, int fileSplitSize, File fileSplitDirectory, int numberOfCopies, int numberOfRetries) { + this.apiHostInsecure = apiHostInsecure; + this.keepWebHost = keepWebHost; + this.keepWebPort = keepWebPort; + this.apiHost = apiHost; + this.apiPort = apiPort; + this.apiToken = apiToken; + this.apiProtocol = apiProtocol; + this.fileSplitSize = fileSplitSize; + this.fileSplitDirectory = fileSplitDirectory; + this.numberOfCopies = numberOfCopies; + this.numberOfRetries = numberOfRetries; + } + + public static ExternalConfigProviderBuilder builder() { + return new ExternalConfigProviderBuilder(); + } + + @Override + public String toString() { + return "ExternalConfigProvider{" + + "apiHostInsecure=" + apiHostInsecure + + ", keepWebHost='" + keepWebHost + '\'' + + ", keepWebPort=" + keepWebPort + + ", apiHost='" + apiHost + '\'' + + ", apiPort=" + apiPort + + ", apiToken='" + apiToken + '\'' + + ", apiProtocol='" + apiProtocol + '\'' + + ", fileSplitSize=" + fileSplitSize + + ", fileSplitDirectory=" + fileSplitDirectory + + ", numberOfCopies=" + numberOfCopies + + ", numberOfRetries=" + numberOfRetries + + '}'; + } + + public boolean isApiHostInsecure() { + return this.apiHostInsecure; + } + + public String getKeepWebHost() { + return this.keepWebHost; + } + + public int getKeepWebPort() { + return this.keepWebPort; + } + + public String getApiHost() { + return this.apiHost; + } + + public int getApiPort() { + return this.apiPort; + } + + public String getApiToken() { + return this.apiToken; + } + + public String getApiProtocol() { + return this.apiProtocol; + } + + public int getFileSplitSize() { + return this.fileSplitSize; + } + + public File getFileSplitDirectory() { + return this.fileSplitDirectory; + } + + public int getNumberOfCopies() { + return this.numberOfCopies; + } + + public int getNumberOfRetries() { + return this.numberOfRetries; + } + + public static class ExternalConfigProviderBuilder { + private boolean apiHostInsecure; + private String keepWebHost; + private int keepWebPort; + private String apiHost; + private int apiPort; + private String apiToken; + private String apiProtocol; + private int fileSplitSize; + private File fileSplitDirectory; + private int numberOfCopies; + private int numberOfRetries; + + ExternalConfigProviderBuilder() { + } + + public ExternalConfigProvider.ExternalConfigProviderBuilder apiHostInsecure(boolean apiHostInsecure) { + this.apiHostInsecure = apiHostInsecure; + return this; + } + + public ExternalConfigProvider.ExternalConfigProviderBuilder keepWebHost(String keepWebHost) { + this.keepWebHost = keepWebHost; + return this; + } + + public ExternalConfigProvider.ExternalConfigProviderBuilder keepWebPort(int keepWebPort) { + this.keepWebPort = keepWebPort; + return this; + } + + public ExternalConfigProvider.ExternalConfigProviderBuilder apiHost(String apiHost) { + this.apiHost = apiHost; + return this; + } + + public ExternalConfigProvider.ExternalConfigProviderBuilder apiPort(int apiPort) { + this.apiPort = apiPort; + return this; + } + + public ExternalConfigProvider.ExternalConfigProviderBuilder apiToken(String apiToken) { + this.apiToken = apiToken; + return this; + } + + public ExternalConfigProvider.ExternalConfigProviderBuilder apiProtocol(String apiProtocol) { + this.apiProtocol = apiProtocol; + return this; + } + + public ExternalConfigProvider.ExternalConfigProviderBuilder fileSplitSize(int fileSplitSize) { + this.fileSplitSize = fileSplitSize; + return this; + } + + public ExternalConfigProvider.ExternalConfigProviderBuilder fileSplitDirectory(File fileSplitDirectory) { + this.fileSplitDirectory = fileSplitDirectory; + return this; + } + + public ExternalConfigProvider.ExternalConfigProviderBuilder numberOfCopies(int numberOfCopies) { + this.numberOfCopies = numberOfCopies; + return this; + } + + public ExternalConfigProvider.ExternalConfigProviderBuilder numberOfRetries(int numberOfRetries) { + this.numberOfRetries = numberOfRetries; + return this; + } + + public ExternalConfigProvider build() { + return new ExternalConfigProvider(apiHostInsecure, keepWebHost, keepWebPort, apiHost, apiPort, apiToken, apiProtocol, fileSplitSize, fileSplitDirectory, numberOfCopies, numberOfRetries); + } + + } +} diff --git a/src/main/java/org/arvados/client/config/FileConfigProvider.java b/src/main/java/org/arvados/client/config/FileConfigProvider.java new file mode 100644 index 0000000000..589c3346b2 --- /dev/null +++ b/src/main/java/org/arvados/client/config/FileConfigProvider.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.config; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; + +import java.io.File; + +public class FileConfigProvider implements ConfigProvider { + + private static final String DEFAULT_PATH = "arvados"; + private final Config config; + + public FileConfigProvider() { + config = ConfigFactory.load().getConfig(DEFAULT_PATH); + } + + public FileConfigProvider(final String configFile) { + config = (configFile != null) ? + ConfigFactory.load(configFile).getConfig(DEFAULT_PATH) : ConfigFactory.load().getConfig(DEFAULT_PATH); + } + + public Config getConfig() { + return config; + } + + private File getFile(String path) { + return new File(config.getString(path)); + } + + private int getInt(String path) { + return config.getInt(path); + } + + private boolean getBoolean(String path) { + return config.getBoolean(path); + } + + private String getString(String path) { + return config.getString(path); + } + + @Override + public boolean isApiHostInsecure() { + return this.getBoolean("api.host-insecure"); + } + + @Override + public String getKeepWebHost() { + return this.getString("api.keepweb-host"); + } + + @Override + public int getKeepWebPort() { + return this.getInt("api.keepweb-port"); + } + + @Override + public String getApiHost() { + return this.getString("api.host"); + } + + @Override + public int getApiPort() { + return this.getInt("api.port"); + } + + @Override + public String getApiToken() { + return this.getString("api.token"); + } + + @Override + public String getApiProtocol() { + return this.getString("api.protocol"); + } + + @Override + public int getFileSplitSize() { + return this.getInt("split-size"); + } + + @Override + public File getFileSplitDirectory() { + return this.getFile("temp-dir"); + } + + @Override + public int getNumberOfCopies() { + return this.getInt("copies"); + } + + @Override + public int getNumberOfRetries() { + return this.getInt("retries"); + } + + public String getIntegrationTestProjectUuid() { + return this.getString("integration-tests.project-uuid"); + } +} diff --git a/src/main/java/org/arvados/client/exception/ArvadosApiException.java b/src/main/java/org/arvados/client/exception/ArvadosApiException.java new file mode 100644 index 0000000000..51a9962487 --- /dev/null +++ b/src/main/java/org/arvados/client/exception/ArvadosApiException.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.exception; + +public class ArvadosApiException extends ArvadosClientException { + + private static final long serialVersionUID = 1L; + + public ArvadosApiException(String message) { + super(message); + } + + public ArvadosApiException(String message, Throwable cause) { + super(message, cause); + } + + public ArvadosApiException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/arvados/client/exception/ArvadosClientException.java b/src/main/java/org/arvados/client/exception/ArvadosClientException.java new file mode 100644 index 0000000000..e93028d75c --- /dev/null +++ b/src/main/java/org/arvados/client/exception/ArvadosClientException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.exception; + +/** + * Parent exception for all exceptions in library. + * More specific exceptions like ArvadosApiException extend this class. + */ +public class ArvadosClientException extends RuntimeException { + + public ArvadosClientException(String message) { + super(message); + } + + public ArvadosClientException(String message, Throwable cause) { + super(message, cause); + } + + public ArvadosClientException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/arvados/client/facade/ArvadosFacade.java b/src/main/java/org/arvados/client/facade/ArvadosFacade.java new file mode 100644 index 0000000000..b80b528fe5 --- /dev/null +++ b/src/main/java/org/arvados/client/facade/ArvadosFacade.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.facade; + +import com.google.common.collect.Lists; +import org.arvados.client.api.client.CollectionsApiClient; +import org.arvados.client.api.client.GroupsApiClient; +import org.arvados.client.api.client.KeepWebApiClient; +import org.arvados.client.api.client.UsersApiClient; +import org.arvados.client.api.model.*; +import org.arvados.client.api.model.argument.Filter; +import org.arvados.client.api.model.argument.ListArgument; +import org.arvados.client.config.FileConfigProvider; +import org.arvados.client.config.ConfigProvider; +import org.arvados.client.logic.collection.FileToken; +import org.arvados.client.logic.collection.ManifestDecoder; +import org.arvados.client.logic.keep.FileDownloader; +import org.arvados.client.logic.keep.FileUploader; +import org.arvados.client.logic.keep.KeepClient; +import org.slf4j.Logger; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class ArvadosFacade { + + private final ConfigProvider config; + private final Logger log = org.slf4j.LoggerFactory.getLogger(ArvadosFacade.class); + private CollectionsApiClient collectionsApiClient; + private GroupsApiClient groupsApiClient; + private UsersApiClient usersApiClient; + private FileDownloader fileDownloader; + private FileUploader fileUploader; + private static final String PROJECT = "project"; + private static final String SUBPROJECT = "sub-project"; + + public ArvadosFacade(ConfigProvider config) { + this.config = config; + setFacadeFields(); + } + + public ArvadosFacade() { + this.config = new FileConfigProvider(); + setFacadeFields(); + } + + private void setFacadeFields() { + collectionsApiClient = new CollectionsApiClient(config); + groupsApiClient = new GroupsApiClient(config); + usersApiClient = new UsersApiClient(config); + KeepClient keepClient = new KeepClient(config); + ManifestDecoder manifestDecoder = new ManifestDecoder(); + KeepWebApiClient keepWebApiClient = new KeepWebApiClient(config); + fileDownloader = new FileDownloader(keepClient, manifestDecoder, collectionsApiClient, keepWebApiClient); + fileUploader = new FileUploader(keepClient, collectionsApiClient, config); + } + + /** + * This method downloads single file from collection using Arvados Keep-Web. + * File is saved on a drive in specified location and returned. + * + * @param filePathName path to the file in collection. If requested file is stored + * directly in collection (not within its subdirectory) this + * would be just the name of file (ex. 'file.txt'). + * Otherwise full file path must be passed (ex. 'folder/file.txt') + * @param collectionUuid uuid of collection containing requested file + * @param pathToDownloadFolder path to location in which file should be saved. + * Passed location must be a directory in which file of + * that name does not already exist. + * @return downloaded file + */ + public File downloadFile(String filePathName, String collectionUuid, String pathToDownloadFolder) { + return fileDownloader.downloadSingleFileUsingKeepWeb(filePathName, collectionUuid, pathToDownloadFolder); + } + + /** + * This method downloads all files from collection. + * Directory named by collection uuid is created in specified location, + * files are saved on a drive in this directory and list with downloaded + * files is returned. + * + * @param collectionUuid uuid of collection from which files are downloaded + * @param pathToDownloadFolder path to location in which files should be saved. + * New folder named by collection uuid, containing + * downloaded files, is created in this location. + * Passed location must be a directory in which folder + * of that name does not already exist. + * @param usingKeepWeb if set to true files will be downloaded using Keep Web. + * If set to false files will be downloaded using Keep Server API. + * @return list containing downloaded files + */ + public List downloadCollectionFiles(String collectionUuid, String pathToDownloadFolder, boolean usingKeepWeb) { + if (usingKeepWeb) + return fileDownloader.downloadFilesFromCollectionUsingKeepWeb(collectionUuid, pathToDownloadFolder); + return fileDownloader.downloadFilesFromCollection(collectionUuid, pathToDownloadFolder); + } + + /** + * Lists all FileTokens (objects containing information about files) for + * specified collection. + * Information in each FileToken includes file path, name, size and position + * in data stream + * + * @param collectionUuid uuid of collection for which FileTokens are listed + * @return list containing FileTokens for each file in specified collection + */ + public List listFileInfoFromCollection(String collectionUuid) { + return fileDownloader.listFileInfoFromCollection(collectionUuid); + } + + /** + * Creates and uploads new collection containing passed files. + * Created collection has a default name and is uploaded to user's 'Home' project. + * + * @see ArvadosFacade#upload(List, String, String) + */ + public Collection upload(List files) { + return upload(files, null, null); + } + + /** + * Creates and uploads new collection containing a single file. + * Created collection has a default name and is uploaded to user's 'Home' project. + * + * @see ArvadosFacade#upload(List, String, String) + */ + public Collection upload(File file) { + return upload(Collections.singletonList(file), null, null); + } + + /** + * Uploads new collection with specified name and containing selected files + * to an existing project. + * + * @param sourceFiles list of files to be uploaded within new collection + * @param collectionName name for the newly created collection. + * Collection with that name cannot be already created + * in specified project. If null is passed + * then collection name is set to default, containing + * phrase 'New Collection' and a timestamp. + * @param projectUuid uuid of the project in which created collection is to be included. + * If null is passed then collection is uploaded to user's 'Home' project. + * @return collection object mapped from JSON that is returned from server after successful upload + */ + public Collection upload(List sourceFiles, String collectionName, String projectUuid) { + return fileUploader.upload(sourceFiles, collectionName, projectUuid); + } + + /** + * Uploads a file to a specified collection. + * + * @see ArvadosFacade#uploadToExistingCollection(List, String) + */ + public Collection uploadToExistingCollection(File file, String collectionUUID) { + return fileUploader.uploadToExistingCollection(Collections.singletonList(file), collectionUUID); + } + + /** + * Uploads multiple files to an existing collection. + * + * @param files list of files to be uploaded to existing collection. + * File names must be unique - both within passed list and + * in comparison with files already existing within collection. + * @param collectionUUID UUID of collection to which files should be uploaded + * @return collection object mapped from JSON that is returned from server after successful upload + */ + public Collection uploadToExistingCollection(List files, String collectionUUID) { + return fileUploader.uploadToExistingCollection(files, collectionUUID); + } + + /** + * Creates and uploads new empty collection to specified project. + * + * @param collectionName name for the newly created collection. + * Collection with that name cannot be already created + * in specified project. + * @param projectUuid uuid of project that will contain uploaded empty collection. + * To select home project pass current user's uuid from getCurrentUser() + * @return collection object mapped from JSON that is returned from server after successful upload + * @see ArvadosFacade#getCurrentUser() + */ + public Collection createEmptyCollection(String collectionName, String projectUuid) { + Collection collection = new Collection(); + collection.setOwnerUuid(projectUuid); + collection.setName(collectionName); + return collectionsApiClient.create(collection); + } + + /** + * Returns current user information based on Api Token provided via configuration + * + * @return user object mapped from JSON that is returned from server based on provided Api Token. + * It contains information about user who has this token assigned. + */ + public User getCurrentUser() { + return usersApiClient.current(); + } + + /** + * Gets uuid of current user based on api Token provided in configuration and uses it to list all + * projects that this user owns in Arvados. + * + * @return GroupList containing all groups that current user is owner of. + * @see ArvadosFacade#getCurrentUser() + */ + public GroupList showGroupsOwnedByCurrentUser() { + ListArgument listArgument = ListArgument.builder() + .filters(Arrays.asList( + Filter.of("owner_uuid", Filter.Operator.LIKE, getCurrentUser().getUuid()), + Filter.of("group_class", Filter.Operator.IN, Lists.newArrayList(PROJECT, SUBPROJECT) + ))) + .build(); + GroupList groupList = groupsApiClient.list(listArgument); + log.debug("Groups owned by user:"); + groupList.getItems().forEach(m -> log.debug(m.getUuid() + " -- " + m.getName())); + + return groupList; + } + + /** + * Gets uuid of current user based on api Token provided in configuration and uses it to list all + * projects that this user has read access to in Arvados. + * + * @return GroupList containing all groups that current user has read access to. + */ + public GroupList showGroupsAccessibleByCurrentUser() { + ListArgument listArgument = ListArgument.builder() + .filters(Collections.singletonList( + Filter.of("group_class", Filter.Operator.IN, Lists.newArrayList(PROJECT, SUBPROJECT) + ))) + .build(); + GroupList groupList = groupsApiClient.list(listArgument); + log.debug("Groups accessible by user:"); + groupList.getItems().forEach(m -> log.debug(m.getUuid() + " -- " + m.getName())); + + return groupList; + } + + /** + * Filters all collections from selected project and returns list of those that contain passed String in their name. + * Operator "LIKE" is used so in order to obtain certain collection it is sufficient to pass just part of its name. + * Returned collections in collectionList are ordered by date of creation (starting from oldest one). + * + * @param collectionName collections containing this param in their name will be returned. + * Passing a wildcard is possible - for example passing "a%" searches for + * all collections starting with "a". + * @param projectUuid uuid of project in which will be searched for collections with given name. To search home + * project provide user uuid (from getCurrentUser()) + * @return object CollectionList containing all collections matching specified name criteria + * @see ArvadosFacade#getCurrentUser() + */ + public CollectionList getCollectionsFromProjectByName(String collectionName, String projectUuid) { + ListArgument listArgument = ListArgument.builder() + .filters(Arrays.asList( + Filter.of("owner_uuid", Filter.Operator.LIKE, projectUuid), + Filter.of("name", Filter.Operator.LIKE, collectionName) + )) + .order(Collections.singletonList("created_at")) + .build(); + + return collectionsApiClient.list(listArgument); + } + + /** + * Creates new project that will be a subproject of "home" for current user. + * + * @param projectName name for the newly created project + * @return Group object containing information about created project + * (mapped from JSON returned from server after creating the project) + */ + public Group createNewProject(String projectName) { + Group project = new Group(); + project.setName(projectName); + project.setGroupClass(PROJECT); + Group createdProject = groupsApiClient.create(project); + log.debug("Project " + createdProject.getName() + " created with UUID: " + createdProject.getUuid()); + return createdProject; + } + + /** + * Deletes collection with specified uuid. + * + * @param collectionUuid uuid of collection to be deleted. User whose token is provided in configuration + * must be authorized to delete such collection. + * @return collection object with deleted collection (mapped from JSON returned from server after deleting the collection) + */ + public Collection deleteCollection(String collectionUuid) { + Collection deletedCollection = collectionsApiClient.delete(collectionUuid); + log.debug("Collection: " + collectionUuid + " deleted."); + return deletedCollection; + } +} diff --git a/src/main/java/org/arvados/client/logic/collection/CollectionFactory.java b/src/main/java/org/arvados/client/logic/collection/CollectionFactory.java new file mode 100644 index 0000000000..25379f54b8 --- /dev/null +++ b/src/main/java/org/arvados/client/logic/collection/CollectionFactory.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.collection; + +import org.arvados.client.api.client.GroupsApiClient; +import org.arvados.client.api.client.UsersApiClient; +import org.arvados.client.exception.ArvadosApiException; +import org.arvados.client.api.model.Collection; +import org.arvados.client.common.Patterns; +import org.arvados.client.config.FileConfigProvider; +import org.arvados.client.config.ConfigProvider; +import org.arvados.client.exception.ArvadosClientException; + +import java.io.File; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Optional; + +public class CollectionFactory { + + private ConfigProvider config; + private UsersApiClient usersApiClient; + private GroupsApiClient groupsApiClient; + + private final String name; + private final String projectUuid; + private final List manifestFiles; + private final List manifestLocators; + + private CollectionFactory(ConfigProvider config, String name, String projectUuid, List manifestFiles, List manifestLocators) { + this.name = name; + this.projectUuid = projectUuid; + this.manifestFiles = manifestFiles; + this.manifestLocators = manifestLocators; + this.config = config; + setApiClients(); + } + + public static CollectionFactoryBuilder builder() { + return new CollectionFactoryBuilder(); + } + + private void setApiClients() { + if(this.config == null) this.config = new FileConfigProvider(); + + this.usersApiClient = new UsersApiClient(config); + this.groupsApiClient = new GroupsApiClient(config); + } + + public Collection create() { + ManifestFactory manifestFactory = ManifestFactory.builder() + .files(manifestFiles) + .locators(manifestLocators) + .build(); + String manifest = manifestFactory.create(); + + Collection newCollection = new Collection(); + newCollection.setName(getNameOrDefault(name)); + newCollection.setManifestText(manifest); + newCollection.setOwnerUuid(getDesiredProjectUuid(projectUuid)); + + return newCollection; + } + + private String getNameOrDefault(String name) { + return Optional.ofNullable(name).orElseGet(() -> { + LocalDateTime dateTime = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("Y-MM-dd HH:mm:ss.SSS"); + return String.format("New Collection (%s)", dateTime.format(formatter)); + }); + } + + public String getDesiredProjectUuid(String projectUuid) { + try { + if (projectUuid == null || projectUuid.length() == 0){ + return usersApiClient.current().getUuid(); + } else if (projectUuid.matches(Patterns.USER_UUID_PATTERN)) { + return usersApiClient.get(projectUuid).getUuid(); + } else if (projectUuid.matches(Patterns.GROUP_UUID_PATTERN)) { + return groupsApiClient.get(projectUuid).getUuid(); + } + } catch (ArvadosApiException e) { + throw new ArvadosClientException(String.format("An error occurred while getting project by UUID %s", projectUuid)); + } + throw new ArvadosClientException(String.format("No project with %s UUID found", projectUuid)); + } + + public static class CollectionFactoryBuilder { + private ConfigProvider config; + private String name; + private String projectUuid; + private List manifestFiles; + private List manifestLocators; + + CollectionFactoryBuilder() { + } + + public CollectionFactoryBuilder config(ConfigProvider config) { + this.config = config; + return this; + } + + public CollectionFactoryBuilder name(String name) { + this.name = name; + return this; + } + + public CollectionFactoryBuilder projectUuid(String projectUuid) { + this.projectUuid = projectUuid; + return this; + } + + public CollectionFactoryBuilder manifestFiles(List manifestFiles) { + this.manifestFiles = manifestFiles; + return this; + } + + public CollectionFactoryBuilder manifestLocators(List manifestLocators) { + this.manifestLocators = manifestLocators; + return this; + } + + public CollectionFactory build() { + return new CollectionFactory(config, name, projectUuid, manifestFiles, manifestLocators); + } + + } +} diff --git a/src/main/java/org/arvados/client/logic/collection/FileToken.java b/src/main/java/org/arvados/client/logic/collection/FileToken.java new file mode 100644 index 0000000000..b41ccd3cdd --- /dev/null +++ b/src/main/java/org/arvados/client/logic/collection/FileToken.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.collection; + +import com.google.common.base.Strings; +import org.arvados.client.common.Characters; + +public class FileToken { + + private int filePosition; + private int fileSize; + private String fileName; + private String path; + + public FileToken(String fileTokenInfo) { + splitFileTokenInfo(fileTokenInfo); + } + + public FileToken(String fileTokenInfo, String path) { + splitFileTokenInfo(fileTokenInfo); + this.path = path; + } + + private void splitFileTokenInfo(String fileTokenInfo) { + String[] tokenPieces = fileTokenInfo.split(":"); + this.filePosition = Integer.parseInt(tokenPieces[0]); + this.fileSize = Integer.parseInt(tokenPieces[1]); + this.fileName = tokenPieces[2].replace(Characters.SPACE, " "); + } + + @Override + public String toString() { + return filePosition + ":" + fileSize + ":" + fileName; + } + + public String getFullPath() { + return Strings.isNullOrEmpty(path) ? fileName : path + fileName; + } + + public int getFilePosition() { + return this.filePosition; + } + + public int getFileSize() { + return this.fileSize; + } + + public String getFileName() { + return this.fileName; + } + + public String getPath() { + return this.path; + } +} diff --git a/src/main/java/org/arvados/client/logic/collection/ManifestDecoder.java b/src/main/java/org/arvados/client/logic/collection/ManifestDecoder.java new file mode 100644 index 0000000000..6a76a4efbe --- /dev/null +++ b/src/main/java/org/arvados/client/logic/collection/ManifestDecoder.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.collection; + +import org.arvados.client.common.Characters; +import org.arvados.client.exception.ArvadosClientException; +import org.arvados.client.logic.keep.KeepLocator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import static java.util.stream.Collectors.toList; +import static org.arvados.client.common.Patterns.FILE_TOKEN_PATTERN; +import static org.arvados.client.common.Patterns.LOCATOR_PATTERN; + +public class ManifestDecoder { + + public List decode(String manifestText) { + + if (manifestText == null || manifestText.isEmpty()) { + throw new ArvadosClientException("Manifest text cannot be empty."); + } + + List manifestStreams = new ArrayList<>(Arrays.asList(manifestText.split("\\n"))); + if (!manifestStreams.get(0).startsWith(". ")) { + throw new ArvadosClientException("Invalid first path component (expecting \".\")"); + } + + return manifestStreams.stream() + .map(this::decodeSingleManifestStream) + .collect(toList()); + } + + private ManifestStream decodeSingleManifestStream(String manifestStream) { + Objects.requireNonNull(manifestStream, "Manifest stream cannot be empty."); + + LinkedList manifestPieces = new LinkedList<>(Arrays.asList(manifestStream.split("\\s+"))); + String streamName = manifestPieces.poll(); + String path = ".".equals(streamName) ? "" : streamName.substring(2).concat(Characters.SLASH); + + List keepLocators = manifestPieces + .stream() + .filter(p -> p.matches(LOCATOR_PATTERN)) + .map(this::getKeepLocator) + .collect(toList()); + + + List fileTokens = manifestPieces.stream() + .skip(keepLocators.size()) + .filter(p -> p.matches(FILE_TOKEN_PATTERN)) + .map(p -> new FileToken(p, path)) + .collect(toList()); + + return new ManifestStream(streamName, keepLocators, fileTokens); + + } + + private KeepLocator getKeepLocator(String locatorString ) { + try { + return new KeepLocator(locatorString); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/org/arvados/client/logic/collection/ManifestFactory.java b/src/main/java/org/arvados/client/logic/collection/ManifestFactory.java new file mode 100644 index 0000000000..96d605dd95 --- /dev/null +++ b/src/main/java/org/arvados/client/logic/collection/ManifestFactory.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.collection; + +import com.google.common.collect.ImmutableList; +import org.arvados.client.common.Characters; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class ManifestFactory { + + private Collection files; + private List locators; + + ManifestFactory(Collection files, List locators) { + this.files = files; + this.locators = locators; + } + + public static ManifestFactoryBuilder builder() { + return new ManifestFactoryBuilder(); + } + + public String create() { + ImmutableList.Builder builder = new ImmutableList.Builder() + .add(Characters.DOT) + .addAll(locators); + long filePosition = 0; + for (File file : files) { + builder.add(String.format("%d:%d:%s", filePosition, file.length(), file.getName().replace(" ", Characters.SPACE))); + filePosition += file.length(); + } + String manifest = builder.build().stream().collect(Collectors.joining(" ")).concat(Characters.NEW_LINE); + return manifest; + } + + public static class ManifestFactoryBuilder { + private Collection files; + private List locators; + + ManifestFactoryBuilder() { + } + + public ManifestFactory.ManifestFactoryBuilder files(Collection files) { + this.files = files; + return this; + } + + public ManifestFactory.ManifestFactoryBuilder locators(List locators) { + this.locators = locators; + return this; + } + + public ManifestFactory build() { + return new ManifestFactory(files, locators); + } + + } +} diff --git a/src/main/java/org/arvados/client/logic/collection/ManifestStream.java b/src/main/java/org/arvados/client/logic/collection/ManifestStream.java new file mode 100644 index 0000000000..30440300e4 --- /dev/null +++ b/src/main/java/org/arvados/client/logic/collection/ManifestStream.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.collection; + +import org.arvados.client.logic.keep.KeepLocator; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ManifestStream { + + private String streamName; + private List keepLocators; + private List fileTokens; + + public ManifestStream(String streamName, List keepLocators, List fileTokens) { + this.streamName = streamName; + this.keepLocators = keepLocators; + this.fileTokens = fileTokens; + } + + @Override + public String toString() { + return streamName + " " + Stream.concat(keepLocators.stream().map(KeepLocator::toString), fileTokens.stream().map(FileToken::toString)) + .collect(Collectors.joining(" ")); + } + + public String getStreamName() { + return this.streamName; + } + + public List getKeepLocators() { + return this.keepLocators; + } + + public List getFileTokens() { + return this.fileTokens; + } +} diff --git a/src/main/java/org/arvados/client/logic/keep/FileDownloader.java b/src/main/java/org/arvados/client/logic/keep/FileDownloader.java new file mode 100644 index 0000000000..1f694f25c2 --- /dev/null +++ b/src/main/java/org/arvados/client/logic/keep/FileDownloader.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.keep; + +import com.google.common.collect.Lists; +import org.arvados.client.api.client.CollectionsApiClient; +import org.arvados.client.api.client.KeepWebApiClient; +import org.arvados.client.api.model.Collection; +import org.arvados.client.common.Characters; +import org.arvados.client.exception.ArvadosClientException; +import org.arvados.client.logic.collection.FileToken; +import org.arvados.client.logic.collection.ManifestDecoder; +import org.arvados.client.logic.collection.ManifestStream; +import org.arvados.client.logic.keep.exception.DownloadFolderAlreadyExistsException; +import org.arvados.client.logic.keep.exception.FileAlreadyExistsException; +import org.slf4j.Logger; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class FileDownloader { + + private final KeepClient keepClient; + private final ManifestDecoder manifestDecoder; + private final CollectionsApiClient collectionsApiClient; + private final KeepWebApiClient keepWebApiClient; + private final Logger log = org.slf4j.LoggerFactory.getLogger(FileDownloader.class); + + public FileDownloader(KeepClient keepClient, ManifestDecoder manifestDecoder, CollectionsApiClient collectionsApiClient, KeepWebApiClient keepWebApiClient) { + this.keepClient = keepClient; + this.manifestDecoder = manifestDecoder; + this.collectionsApiClient = collectionsApiClient; + this.keepWebApiClient = keepWebApiClient; + } + + public List listFileInfoFromCollection(String collectionUuid) { + Collection requestedCollection = collectionsApiClient.get(collectionUuid); + String manifestText = requestedCollection.getManifestText(); + + // decode manifest text and get list of all FileTokens for this collection + return manifestDecoder.decode(manifestText) + .stream() + .flatMap(p -> p.getFileTokens().stream()) + .collect(Collectors.toList()); + } + + public File downloadSingleFileUsingKeepWeb(String filePathName, String collectionUuid, String pathToDownloadFolder) { + FileToken fileToken = getFileTokenFromCollection(filePathName, collectionUuid); + if (fileToken == null) { + throw new ArvadosClientException(String.format("%s not found in Collection with UUID %s", filePathName, collectionUuid)); + } + + File downloadedFile = checkIfFileExistsInTargetLocation(fileToken, pathToDownloadFolder); + try (FileOutputStream fos = new FileOutputStream(downloadedFile)) { + fos.write(keepWebApiClient.download(collectionUuid, filePathName)); + } catch (IOException e) { + throw new ArvadosClientException(String.format("Unable to write down file %s", fileToken.getFileName()), e); + } + return downloadedFile; + } + + public List downloadFilesFromCollectionUsingKeepWeb(String collectionUuid, String pathToDownloadFolder) { + String collectionTargetDir = setTargetDirectory(collectionUuid, pathToDownloadFolder).getAbsolutePath(); + List fileTokens = listFileInfoFromCollection(collectionUuid); + + List> futures = Lists.newArrayList(); + for (FileToken fileToken : fileTokens) { + futures.add(CompletableFuture.supplyAsync(() -> this.downloadOneFileFromCollectionUsingKeepWeb(fileToken, collectionUuid, collectionTargetDir))); + } + + @SuppressWarnings("unchecked") + CompletableFuture[] array = futures.toArray(new CompletableFuture[0]); + return Stream.of(array) + .map(CompletableFuture::join).collect(Collectors.toList()); + } + + private FileToken getFileTokenFromCollection(String filePathName, String collectionUuid) { + return listFileInfoFromCollection(collectionUuid) + .stream() + .filter(p -> (p.getFullPath()).equals(filePathName)) + .findFirst() + .orElse(null); + } + + private File checkIfFileExistsInTargetLocation(FileToken fileToken, String pathToDownloadFolder) { + String fileName = fileToken.getFileName(); + + File downloadFile = new File(pathToDownloadFolder + Characters.SLASH + fileName); + if (downloadFile.exists()) { + throw new FileAlreadyExistsException(String.format("File %s exists in location %s", fileName, pathToDownloadFolder)); + } else { + return downloadFile; + } + } + + private File downloadOneFileFromCollectionUsingKeepWeb(FileToken fileToken, String collectionUuid, String pathToDownloadFolder) { + String filePathName = fileToken.getPath() + fileToken.getFileName(); + File downloadedFile = new File(pathToDownloadFolder + Characters.SLASH + filePathName); + downloadedFile.getParentFile().mkdirs(); + + try (FileOutputStream fos = new FileOutputStream(downloadedFile)) { + fos.write(keepWebApiClient.download(collectionUuid, filePathName)); + } catch (IOException e) { + throw new RuntimeException(e); + } + return downloadedFile; + } + + public List downloadFilesFromCollection(String collectionUuid, String pathToDownloadFolder) { + + // download requested collection and extract manifest text + Collection requestedCollection = collectionsApiClient.get(collectionUuid); + String manifestText = requestedCollection.getManifestText(); + + // if directory with this collectionUUID does not exist - create one + // if exists - abort (throw exception) + File collectionTargetDir = setTargetDirectory(collectionUuid, pathToDownloadFolder); + + // decode manifest text and create list of ManifestStream objects containing KeepLocators and FileTokens + List manifestStreams = manifestDecoder.decode(manifestText); + + //list of all downloaded files that will be returned by this method + List downloadedFilesFromCollection = new ArrayList<>(); + + // download files for each manifest stream + for (ManifestStream manifestStream : manifestStreams) + downloadedFilesFromCollection.addAll(downloadFilesFromSingleManifestStream(manifestStream, collectionTargetDir)); + + log.debug(String.format("Total of: %d files downloaded", downloadedFilesFromCollection.size())); + return downloadedFilesFromCollection; + } + + private File setTargetDirectory(String collectionUUID, String pathToDownloadFolder) { + //local directory to save downloaded files + File collectionTargetDir = new File(pathToDownloadFolder + Characters.SLASH + collectionUUID); + if (collectionTargetDir.exists()) { + throw new DownloadFolderAlreadyExistsException(String.format("Directory for collection UUID %s already exists", collectionUUID)); + } else { + collectionTargetDir.mkdirs(); + } + return collectionTargetDir; + } + + private List downloadFilesFromSingleManifestStream(ManifestStream manifestStream, File collectionTargetDir){ + List downloadedFiles = new ArrayList<>(); + List keepLocators = manifestStream.getKeepLocators(); + DownloadHelper downloadHelper = new DownloadHelper(keepLocators); + + for (FileToken fileToken : manifestStream.getFileTokens()) { + File downloadedFile = new File(collectionTargetDir.getAbsolutePath() + Characters.SLASH + fileToken.getFullPath()); //create file + downloadedFile.getParentFile().mkdirs(); + + try (FileOutputStream fos = new FileOutputStream(downloadedFile, true)) { + downloadHelper.setBytesToDownload(fileToken.getFileSize()); //update file size info + + //this part needs to be repeated for each file until whole file is downloaded + do { + downloadHelper.requestNewDataChunk(); //check if new data chunk needs to be downloaded + downloadHelper.writeDownFile(fos); // download data from chunk + } while (downloadHelper.getBytesToDownload() != 0); + + } catch (IOException | ArvadosClientException e) { + throw new ArvadosClientException(String.format("Unable to write down file %s", fileToken.getFileName()), e); + } + + downloadedFiles.add(downloadedFile); + log.debug(String.format("File %d / %d downloaded from manifest stream", + manifestStream.getFileTokens().indexOf(fileToken) + 1, + manifestStream.getFileTokens().size())); + } + return downloadedFiles; + } + + private class DownloadHelper { + + // values for tracking file output streams and matching data chunks with initial files + int currentDataChunkNumber; + int bytesDownloadedFromChunk; + int bytesToDownload; + byte[] currentDataChunk; + boolean remainingDataInChunk; + final List keepLocators; + + private DownloadHelper(List keepLocators) { + currentDataChunkNumber = -1; + bytesDownloadedFromChunk = 0; + remainingDataInChunk = false; + this.keepLocators = keepLocators; + } + + private int getBytesToDownload() { + return bytesToDownload; + } + + private void setBytesToDownload(int bytesToDownload) { + this.bytesToDownload = bytesToDownload; + } + + private void requestNewDataChunk() { + if (!remainingDataInChunk) { + currentDataChunkNumber++; + if (currentDataChunkNumber < keepLocators.size()) { + //swap data chunk for next one + currentDataChunk = keepClient.getDataChunk(keepLocators.get(currentDataChunkNumber)); + log.debug(String.format("%d of %d data chunks from manifest stream downloaded", currentDataChunkNumber + 1, keepLocators.size())); + } else { + throw new ArvadosClientException("Data chunk required for download is missing."); + } + } + } + + private void writeDownFile(FileOutputStream fos) throws IOException { + //case 1: more bytes needed than available in current chunk (or whole current chunk needed) to download file + if (bytesToDownload >= currentDataChunk.length - bytesDownloadedFromChunk) { + writeDownWholeDataChunk(fos); + } + //case 2: current data chunk contains more bytes than is needed for this file + else { + writeDownDataChunkPartially(fos); + } + } + + private void writeDownWholeDataChunk(FileOutputStream fos) throws IOException { + // write all remaining bytes from current chunk + fos.write(currentDataChunk, bytesDownloadedFromChunk, currentDataChunk.length - bytesDownloadedFromChunk); + //update bytesToDownload + bytesToDownload -= (currentDataChunk.length - bytesDownloadedFromChunk); + // set remaining data in chunk to false + remainingDataInChunk = false; + //reset bytesDownloadedFromChunk so that its set to 0 for the next chunk + bytesDownloadedFromChunk = 0; + } + + private void writeDownDataChunkPartially(FileOutputStream fos) throws IOException { + //write all remaining bytes for this file from current chunk + fos.write(currentDataChunk, bytesDownloadedFromChunk, bytesToDownload); + // update number of bytes downloaded from this chunk + bytesDownloadedFromChunk += bytesToDownload; + // set remaining data in chunk to true + remainingDataInChunk = true; + // reset bytesToDownload to exit while loop and move to the next file + bytesToDownload = 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/arvados/client/logic/keep/FileTransferHandler.java b/src/main/java/org/arvados/client/logic/keep/FileTransferHandler.java new file mode 100644 index 0000000000..c6a8ad3687 --- /dev/null +++ b/src/main/java/org/arvados/client/logic/keep/FileTransferHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.keep; + +import org.arvados.client.api.client.KeepServerApiClient; +import org.arvados.client.exception.ArvadosApiException; +import org.arvados.client.config.ConfigProvider; +import org.slf4j.Logger; + +import java.io.File; +import java.util.Map; + +public class FileTransferHandler { + + private final String host; + private final KeepServerApiClient keepServerApiClient; + private final Map headers; + private final Logger log = org.slf4j.LoggerFactory.getLogger(FileTransferHandler.class); + + public FileTransferHandler(String host, Map headers, ConfigProvider config) { + this.host = host; + this.headers = headers; + this.keepServerApiClient = new KeepServerApiClient(config); + } + + public String put(String hashString, File body) { + String url = host + hashString; + String locator = null; + try { + locator = keepServerApiClient.upload(url, headers, body); + } catch (ArvadosApiException e) { + log.error("Cannot upload file to Keep server.", e); + } + return locator; + } + + public byte[] get(KeepLocator locator) { + return get(locator.stripped(), locator.permissionHint()); + } + + public byte[] get(String blockLocator, String authToken) { + String url = host + blockLocator + "+" + authToken; + try { + return keepServerApiClient.download(url); + } catch (ArvadosApiException e) { + log.error("Cannot download file from Keep server.", e); + return null; + } + } +} diff --git a/src/main/java/org/arvados/client/logic/keep/FileUploader.java b/src/main/java/org/arvados/client/logic/keep/FileUploader.java new file mode 100644 index 0000000000..52e0f66caf --- /dev/null +++ b/src/main/java/org/arvados/client/logic/keep/FileUploader.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.keep; + +import com.google.common.collect.Lists; +import org.arvados.client.api.client.CollectionsApiClient; +import org.arvados.client.api.model.Collection; +import org.arvados.client.common.Characters; +import org.arvados.client.config.ConfigProvider; +import org.arvados.client.exception.ArvadosClientException; +import org.arvados.client.logic.collection.CollectionFactory; +import org.arvados.client.utils.FileMerge; +import org.arvados.client.utils.FileSplit; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import static java.util.stream.Collectors.toList; + +public class FileUploader { + + private final KeepClient keepClient; + private final CollectionsApiClient collectionsApiClient; + private final ConfigProvider config; + private final Logger log = org.slf4j.LoggerFactory.getLogger(FileUploader.class); + + public FileUploader(KeepClient keepClient, CollectionsApiClient collectionsApiClient, ConfigProvider config) { + this.keepClient = keepClient; + this.collectionsApiClient = collectionsApiClient; + this.config = config; + } + + public Collection upload(List sourceFiles, String collectionName, String projectUuid) { + List locators = uploadToKeep(sourceFiles); + CollectionFactory collectionFactory = CollectionFactory.builder() + .config(config) + .name(collectionName) + .projectUuid(projectUuid) + .manifestFiles(sourceFiles) + .manifestLocators(locators) + .build(); + + Collection newCollection = collectionFactory.create(); + return collectionsApiClient.create(newCollection); + } + + public Collection uploadToExistingCollection(List files, String collectionUuid) { + List locators = uploadToKeep(files); + Collection collectionBeforeUpload = collectionsApiClient.get(collectionUuid); + String oldManifest = collectionBeforeUpload.getManifestText(); + + CollectionFactory collectionFactory = CollectionFactory.builder() + .config(config) + .manifestFiles(files) + .manifestLocators(locators).build(); + + String newPartOfManifestText = collectionFactory.create().getManifestText(); + String newManifest = oldManifest + newPartOfManifestText; + + collectionBeforeUpload.setManifestText(newManifest); + return collectionsApiClient.update(collectionBeforeUpload); + } + + private List uploadToKeep(List files) { + File targetDir = config.getFileSplitDirectory(); + File combinedFile = new File(targetDir.getAbsolutePath() + Characters.SLASH + UUID.randomUUID()); + List chunks; + try { + FileMerge.merge(files, combinedFile); + chunks = FileSplit.split(combinedFile, targetDir, config.getFileSplitSize()); + } catch (IOException e) { + throw new ArvadosClientException("Cannot create file chunks for upload", e); + } + combinedFile.delete(); + + int copies = config.getNumberOfCopies(); + int numRetries = config.getNumberOfRetries(); + + List locators = Lists.newArrayList(); + for (File chunk : chunks) { + try { + locators.add(keepClient.put(chunk, copies, numRetries)); + } catch (ArvadosClientException e) { + log.error("Problem occurred while uploading chunk file {}", chunk.getName(), e); + throw e; + } + } + return locators.stream() + .filter(Objects::nonNull) + .collect(toList()); + } +} diff --git a/src/main/java/org/arvados/client/logic/keep/KeepClient.java b/src/main/java/org/arvados/client/logic/keep/KeepClient.java new file mode 100644 index 0000000000..9cc732d46d --- /dev/null +++ b/src/main/java/org/arvados/client/logic/keep/KeepClient.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.keep; + +import com.google.common.collect.Lists; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.arvados.client.api.client.KeepServicesApiClient; +import org.arvados.client.api.model.KeepService; +import org.arvados.client.api.model.KeepServiceList; +import org.arvados.client.common.Characters; +import org.arvados.client.common.Headers; +import org.arvados.client.config.ConfigProvider; +import org.arvados.client.exception.ArvadosApiException; +import org.arvados.client.exception.ArvadosClientException; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class KeepClient { + + private final KeepServicesApiClient keepServicesApiClient; + private final Logger log = org.slf4j.LoggerFactory.getLogger(KeepClient.class); + private List keepServices; + private List writableServices; + private Map gatewayServices; + private final String apiToken; + private Integer maxReplicasPerService; + private final ConfigProvider config; + + public KeepClient(ConfigProvider config) { + this.config = config; + keepServicesApiClient = new KeepServicesApiClient(config); + apiToken = config.getApiToken(); + } + + public byte[] getDataChunk(KeepLocator keepLocator) { + + Map headers = new HashMap<>(); + Map rootsMap = new HashMap<>(); + + List sortedRoots = mapNewServices(rootsMap, keepLocator, false, false, headers); + + byte[] dataChunk = sortedRoots + .stream() + .map(rootsMap::get) + .map(r -> r.get(keepLocator)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + + if (dataChunk == null) { + throw new ArvadosClientException("No server responding. Unable to download data chunk."); + } + + return dataChunk; + } + + public String put(File data, int copies, int numRetries) { + + byte[] fileBytes; + try { + fileBytes = FileUtils.readFileToByteArray(data); + } catch (IOException e) { + throw new ArvadosClientException("An error occurred while reading data chunk", e); + } + + String dataHash = DigestUtils.md5Hex(fileBytes); + String locatorString = String.format("%s+%d", dataHash, data.length()); + + if (copies < 1) { + return locatorString; + } + KeepLocator locator = new KeepLocator(locatorString); + + // Tell the proxy how many copies we want it to store + Map headers = new HashMap<>(); + headers.put(Headers.X_KEEP_DESIRED_REPLICAS, String.valueOf(copies)); + + Map rootsMap = new HashMap<>(); + List sortedRoots = mapNewServices(rootsMap, locator, false, true, headers); + + int numThreads = 0; + if (maxReplicasPerService == null || maxReplicasPerService >= copies) { + numThreads = 1; + } else { + numThreads = ((Double) Math.ceil(1.0 * copies / maxReplicasPerService)).intValue(); + } + log.debug("Pool max threads is {}", numThreads); + + List> futures = Lists.newArrayList(); + for (int i = 0; i < numThreads; i++) { + String root = sortedRoots.get(i); + FileTransferHandler keepServiceLocal = rootsMap.get(root); + futures.add(CompletableFuture.supplyAsync(() -> keepServiceLocal.put(dataHash, data))); + } + + @SuppressWarnings("unchecked") + CompletableFuture[] array = futures.toArray(new CompletableFuture[0]); + + return Stream.of(array) + .map(CompletableFuture::join) + .reduce((a, b) -> b) + .orElse(null); + } + + private List mapNewServices(Map rootsMap, KeepLocator locator, + boolean forceRebuild, boolean needWritable, Map headers) { + + headers.putIfAbsent("Authorization", String.format("OAuth2 %s", apiToken)); + List localRoots = weightedServiceRoots(locator, forceRebuild, needWritable); + for (String root : localRoots) { + FileTransferHandler keepServiceLocal = new FileTransferHandler(root, headers, config); + rootsMap.putIfAbsent(root, keepServiceLocal); + } + return localRoots; + } + + /** + * Return an array of Keep service endpoints, in the order in which they should be probed when reading or writing + * data with the given hash+hints. + */ + private List weightedServiceRoots(KeepLocator locator, boolean forceRebuild, boolean needWritable) { + + buildServicesList(forceRebuild); + + List sortedRoots = new ArrayList<>(); + + // Use the services indicated by the given +K@... remote + // service hints, if any are present and can be resolved to a + // URI. + // + for (String hint : locator.getHints()) { + if (hint.startsWith("K@")) { + if (hint.length() == 7) { + sortedRoots.add(String.format("https://keep.%s.arvadosapi.com/", hint.substring(2))); + } else if (hint.length() == 29) { + KeepService svc = gatewayServices.get(hint.substring(2)); + if (svc != null) { + sortedRoots.add(svc.getServiceRoot()); + } + } + } + } + + // Sort the available local services by weight (heaviest first) + // for this locator, and return their service_roots (base URIs) + // in that order. + List useServices = keepServices; + if (needWritable) { + useServices = writableServices; + } + anyNonDiskServices(useServices); + + sortedRoots.addAll(useServices + .stream() + .sorted((ks1, ks2) -> serviceWeight(locator.getMd5sum(), ks2.getUuid()) + .compareTo(serviceWeight(locator.getMd5sum(), ks1.getUuid()))) + .map(KeepService::getServiceRoot) + .collect(Collectors.toList())); + + return sortedRoots; + } + + private void buildServicesList(boolean forceRebuild) { + if (keepServices != null && !forceRebuild) { + return; + } + KeepServiceList keepServiceList; + try { + keepServiceList = keepServicesApiClient.accessible(); + } catch (ArvadosApiException e) { + throw new ArvadosClientException("Cannot obtain list of accessible keep services"); + } + // Gateway services are only used when specified by UUID, + // so there's nothing to gain by filtering them by + // service_type. + gatewayServices = keepServiceList.getItems().stream().collect(Collectors.toMap(KeepService::getUuid, Function.identity())); + + if (gatewayServices.isEmpty()) { + throw new ArvadosClientException("No gateway services available!"); + } + + // Precompute the base URI for each service. + for (KeepService keepService : gatewayServices.values()) { + String serviceHost = keepService.getServiceHost(); + if (!serviceHost.startsWith("[") && serviceHost.contains(Characters.COLON)) { + // IPv6 URIs must be formatted like http://[::1]:80/... + serviceHost = String.format("[%s]", serviceHost); + } + + String protocol = keepService.getServiceSslFlag() ? "https" : "http"; + String serviceRoot = String.format("%s://%s:%d/", protocol, serviceHost, keepService.getServicePort()); + keepService.setServiceRoot(serviceRoot); + } + + keepServices = gatewayServices.values().stream().filter(ks -> !ks.getServiceType().startsWith("gateway:")).collect(Collectors.toList()); + writableServices = keepServices.stream().filter(ks -> !ks.getReadOnly()).collect(Collectors.toList()); + + // For disk type services, max_replicas_per_service is 1 + // It is unknown (unlimited) for other service types. + if (anyNonDiskServices(writableServices)) { + maxReplicasPerService = null; + } else { + maxReplicasPerService = 1; + } + } + + private Boolean anyNonDiskServices(List useServices) { + return useServices.stream().anyMatch(ks -> !ks.getServiceType().equals("disk")); + } + + /** + * Compute the weight of a Keep service endpoint for a data block with a known hash. + *

+ * The weight is md5(h + u) where u is the last 15 characters of the service endpoint's UUID. + */ + private static String serviceWeight(String dataHash, String serviceUuid) { + String shortenedUuid; + if (serviceUuid != null && serviceUuid.length() >= 15) { + int substringIndex = serviceUuid.length() - 15; + shortenedUuid = serviceUuid.substring(substringIndex); + } else { + shortenedUuid = (serviceUuid == null) ? "" : serviceUuid; + } + return DigestUtils.md5Hex(dataHash + shortenedUuid); + } + +} diff --git a/src/main/java/org/arvados/client/logic/keep/KeepLocator.java b/src/main/java/org/arvados/client/logic/keep/KeepLocator.java new file mode 100644 index 0000000000..4d3d42523c --- /dev/null +++ b/src/main/java/org/arvados/client/logic/keep/KeepLocator.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.keep; + +import org.arvados.client.exception.ArvadosClientException; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.arvados.client.common.Patterns.HINT_PATTERN; + +public class KeepLocator { + + private final List hints = new ArrayList<>(); + private String permSig; + private LocalDateTime permExpiry; + private final String md5sum; + private final Integer size; + + public KeepLocator(String locatorString) { + LinkedList pieces = new LinkedList<>(Arrays.asList(locatorString.split("\\+"))); + + md5sum = pieces.poll(); + size = Integer.valueOf(Objects.requireNonNull(pieces.poll())); + + for (String hint : pieces) { + if (!hint.matches(HINT_PATTERN)) { + throw new ArvadosClientException(String.format("invalid hint format: %s", hint)); + } else if (hint.startsWith("A")) { + parsePermissionHint(hint); + } else { + hints.add(hint); + } + } + } + + public List getHints() { + return hints; + } + + public String getMd5sum() { + return md5sum; + } + + @Override + public String toString() { + return Stream.concat(Stream.of(md5sum, size.toString(), permissionHint()), hints.stream()) + .filter(Objects::nonNull) + .collect(Collectors.joining("+")); + } + + public String stripped() { + return size != null ? String.format("%s+%d", md5sum, size) : md5sum; + } + + public String permissionHint() { + if (permSig == null || permExpiry == null) { + return null; + } + + long timestamp = permExpiry.toEpochSecond(ZoneOffset.UTC); + String signTimestamp = Long.toHexString(timestamp); + return String.format("A%s@%s", permSig, signTimestamp); + } + + private void parsePermissionHint(String hint) { + String[] hintSplit = hint.substring(1).split("@", 2); + permSig = hintSplit[0]; + + int permExpiryDecimal = Integer.parseInt(hintSplit[1], 16); + permExpiry = LocalDateTime.ofInstant(Instant.ofEpochSecond(permExpiryDecimal), ZoneOffset.UTC); + } +} diff --git a/src/main/java/org/arvados/client/logic/keep/exception/DownloadFolderAlreadyExistsException.java b/src/main/java/org/arvados/client/logic/keep/exception/DownloadFolderAlreadyExistsException.java new file mode 100644 index 0000000000..9968ff0daf --- /dev/null +++ b/src/main/java/org/arvados/client/logic/keep/exception/DownloadFolderAlreadyExistsException.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.keep.exception; + +import org.arvados.client.exception.ArvadosClientException; + +/** + * Exception indicating that directory with given name was already created in specified location. + * + *

This exception will be thrown during an attempt to download all files from certain + * collection to a location that already contains folder named by this collection's UUID.

+ */ +public class DownloadFolderAlreadyExistsException extends ArvadosClientException { + + public DownloadFolderAlreadyExistsException(String message) { + super(message); + } + +} diff --git a/src/main/java/org/arvados/client/logic/keep/exception/FileAlreadyExistsException.java b/src/main/java/org/arvados/client/logic/keep/exception/FileAlreadyExistsException.java new file mode 100644 index 0000000000..ea02ffc84d --- /dev/null +++ b/src/main/java/org/arvados/client/logic/keep/exception/FileAlreadyExistsException.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.keep.exception; + +import org.arvados.client.exception.ArvadosClientException; + +/** + * Signals that an attempt to download a file with given name has failed for a specified + * download location. + * + *

This exception will be thrown during an attempt to download single file to a location + * that already contains file with given name

+ */ +public class FileAlreadyExistsException extends ArvadosClientException { + + public FileAlreadyExistsException(String message) { super(message); } + +} diff --git a/src/main/java/org/arvados/client/utils/FileMerge.java b/src/main/java/org/arvados/client/utils/FileMerge.java new file mode 100644 index 0000000000..eaabbaaad6 --- /dev/null +++ b/src/main/java/org/arvados/client/utils/FileMerge.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.utils; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collection; + +public class FileMerge { + + public static void merge(Collection files, File targetFile) throws IOException { + try (FileOutputStream fos = new FileOutputStream(targetFile); BufferedOutputStream mergingStream = new BufferedOutputStream(fos)) { + for (File file : files) { + Files.copy(file.toPath(), mergingStream); + } + } + } +} diff --git a/src/main/java/org/arvados/client/utils/FileSplit.java b/src/main/java/org/arvados/client/utils/FileSplit.java new file mode 100644 index 0000000000..e118edc026 --- /dev/null +++ b/src/main/java/org/arvados/client/utils/FileSplit.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.utils; + +import org.apache.commons.io.FileUtils; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Based on: + * {@link} https://stackoverflow.com/questions/10864317/how-to-break-a-file-into-pieces-using-java + */ +public class FileSplit { + + public static List split(File f, File dir, int splitSize) throws IOException { + int partCounter = 1; + + long sizeOfFiles = splitSize * FileUtils.ONE_MB; + byte[] buffer = new byte[(int) sizeOfFiles]; + + List files = new ArrayList<>(); + String fileName = f.getName(); + + try (FileInputStream fis = new FileInputStream(f); BufferedInputStream bis = new BufferedInputStream(fis)) { + int bytesAmount = 0; + while ((bytesAmount = bis.read(buffer)) > 0) { + String filePartName = String.format("%s.%03d", fileName, partCounter++); + File newFile = new File(dir, filePartName); + try (FileOutputStream out = new FileOutputStream(newFile)) { + out.write(buffer, 0, bytesAmount); + } + files.add(newFile); + } + } + return files; + } +} \ No newline at end of file diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf new file mode 100644 index 0000000000..3ff2bb0a98 --- /dev/null +++ b/src/main/resources/reference.conf @@ -0,0 +1,23 @@ +# Arvados client default configuration +# +# Remarks: +# * While providing data remove apostrophes ("") from each line +# * See Arvados documentation for information how to obtain a token: +# https://doc.arvados.org/user/reference/api-tokens.html +# + +arvados { + api { + keepweb-host = localhost + keepweb-port = 8000 + host = localhost + port = 8000 + token = "" + protocol = https + host-insecure = false + } + split-size = 64 + temp-dir = /tmp/file-split + copies = 2 + retries = 0 +} diff --git a/src/test/java/org/arvados/client/api/client/BaseStandardApiClientTest.java b/src/test/java/org/arvados/client/api/client/BaseStandardApiClientTest.java new file mode 100644 index 0000000000..73b559afd4 --- /dev/null +++ b/src/test/java/org/arvados/client/api/client/BaseStandardApiClientTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import okhttp3.HttpUrl; +import org.arvados.client.api.model.Item; +import org.arvados.client.api.model.ItemList; +import org.arvados.client.test.utils.ArvadosClientUnitTest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class BaseStandardApiClientTest extends ArvadosClientUnitTest { + + @Spy + private BaseStandardApiClient client = new BaseStandardApiClient(CONFIG) { + @Override + String getResource() { + return "resource"; + } + + @Override + Class getType() { + return null; + } + + @Override + Class getListType() { + return null; + } + }; + + @Test + public void urlBuilderBuildsExpectedUrlFormat() { + // when + HttpUrl.Builder actual = client.getUrlBuilder(); + + // then + assertThat(actual.build().toString()).isEqualTo("http://localhost:9000/arvados/v1/resource"); + } +} diff --git a/src/test/java/org/arvados/client/api/client/CollectionsApiClientTest.java b/src/test/java/org/arvados/client/api/client/CollectionsApiClientTest.java new file mode 100644 index 0000000000..8da3bfbf51 --- /dev/null +++ b/src/test/java/org/arvados/client/api/client/CollectionsApiClientTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import okhttp3.mockwebserver.RecordedRequest; +import org.arvados.client.api.model.Collection; +import org.arvados.client.api.model.CollectionList; +import org.arvados.client.test.utils.RequestMethod; +import org.arvados.client.test.utils.ArvadosClientMockedWebServerTest; +import org.junit.Test; + +import static org.arvados.client.test.utils.ApiClientTestUtils.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class CollectionsApiClientTest extends ArvadosClientMockedWebServerTest { + + private static final String RESOURCE = "collections"; + + private CollectionsApiClient client = new CollectionsApiClient(CONFIG); + + @Test + public void listCollections() throws Exception { + + // given + server.enqueue(getResponse("collections-list")); + + // when + CollectionList actual = client.list(); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE); + assertRequestMethod(request, RequestMethod.GET); + assertThat(actual.getItemsAvailable()).isEqualTo(41); + } + + @Test + public void getCollection() throws Exception { + + // given + server.enqueue(getResponse("collections-get")); + + String uuid = "112ci-4zz18-p51w7z3fpopo6sm"; + + // when + Collection actual = client.get(uuid); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE + "/" + uuid); + assertRequestMethod(request, RequestMethod.GET); + assertThat(actual.getUuid()).isEqualTo(uuid); + assertThat(actual.getPortableDataHash()).isEqualTo("6c4106229b08fe25f48b3a7a8289dd46+143"); + } + + @Test + public void createCollection() throws Exception { + + // given + server.enqueue(getResponse("collections-create-simple")); + + String name = "Super Collection"; + + Collection collection = new Collection(); + collection.setName(name); + + // when + Collection actual = client.create(collection); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE); + assertRequestMethod(request, RequestMethod.POST); + assertThat(actual.getName()).isEqualTo(name); + assertThat(actual.getPortableDataHash()).isEqualTo("d41d8cd98f00b204e9800998ecf8427e+0"); + assertThat(actual.getManifestText()).isEmpty(); + } + + @Test + public void createCollectionWithManifest() throws Exception { + + // given + server.enqueue(getResponse("collections-create-manifest")); + + String name = "Super Collection"; + String manifestText = ". 7df44272090cee6c0732382bba415ee9+70+Aa5ece4560e3329315165b36c239b8ab79c888f8a@5a1d5708 0:70:README.md\n"; + + Collection collection = new Collection(); + collection.setName(name); + collection.setManifestText(manifestText); + + // when + Collection actual = client.create(collection); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE); + assertRequestMethod(request, RequestMethod.POST); + assertThat(actual.getName()).isEqualTo(name); + assertThat(actual.getPortableDataHash()).isEqualTo("d41d8cd98f00b204e9800998ecf8427e+0"); + assertThat(actual.getManifestText()).isEqualTo(manifestText); + } +} diff --git a/src/test/java/org/arvados/client/api/client/GroupsApiClientTest.java b/src/test/java/org/arvados/client/api/client/GroupsApiClientTest.java new file mode 100644 index 0000000000..6bb385a4ca --- /dev/null +++ b/src/test/java/org/arvados/client/api/client/GroupsApiClientTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import com.google.common.collect.Lists; +import okhttp3.mockwebserver.RecordedRequest; +import org.arvados.client.api.model.Group; +import org.arvados.client.api.model.GroupList; +import org.arvados.client.api.model.argument.Filter; +import org.arvados.client.api.model.argument.ListArgument; +import org.arvados.client.test.utils.RequestMethod; +import org.arvados.client.test.utils.ArvadosClientMockedWebServerTest; +import org.junit.Test; + +import java.util.Arrays; + +import static org.arvados.client.test.utils.ApiClientTestUtils.*; +import static org.junit.Assert.assertEquals; + +public class GroupsApiClientTest extends ArvadosClientMockedWebServerTest { + private static final String RESOURCE = "groups"; + private GroupsApiClient client = new GroupsApiClient(CONFIG); + + @Test + public void listGroups() throws Exception { + + // given + server.enqueue(getResponse("groups-list")); + + // when + GroupList actual = client.list(); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE); + assertRequestMethod(request, RequestMethod.GET); + assertEquals(20, actual.getItems().size()); + } + + @Test + public void listProjectsByOwner() throws Exception { + + // given + server.enqueue(getResponse("groups-list")); + String ownerUuid = "ardev-tpzed-n3kzq4fvoks3uw4"; + String filterSubPath = "?filters=[%20[%20%22owner_uuid%22,%20%22like%22,%20%22ardev-tpzed-n3kzq4fvoks3uw4%22%20],%20" + + "[%20%22group_class%22,%20%22in%22,%20[%20%22project%22,%20%22sub-project%22%20]%20]%20]"; + + // when + ListArgument listArgument = ListArgument.builder() + .filters(Arrays.asList( + Filter.of("owner_uuid", Filter.Operator.LIKE, ownerUuid), + Filter.of("group_class", Filter.Operator.IN, Lists.newArrayList("project", "sub-project") + ))) + .build(); + GroupList actual = client.list(listArgument); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE + filterSubPath); + assertRequestMethod(request, RequestMethod.GET); + assertEquals(20, actual.getItems().size()); + } + + @Test + public void getGroup() throws Exception { + + // given + server.enqueue(getResponse("groups-get")); + + String uuid = "ardev-j7d0g-bmg3pfqtx3ivczp"; + + // when + Group actual = client.get(uuid); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE + "/" + uuid); + assertRequestMethod(request, RequestMethod.GET); + assertEquals(uuid, actual.getUuid()); + assertEquals("3hw0vk4mbl0ofvia5k6x4dwrx", actual.getEtag()); + assertEquals("ardev-tpzed-n3kzq4fvoks3uw4", actual.getOwnerUuid()); + assertEquals("TestGroup1", actual.getName()); + assertEquals("project", actual.getGroupClass()); + + } +} diff --git a/src/test/java/org/arvados/client/api/client/KeepServerApiClientTest.java b/src/test/java/org/arvados/client/api/client/KeepServerApiClientTest.java new file mode 100644 index 0000000000..50a9cc1a6d --- /dev/null +++ b/src/test/java/org/arvados/client/api/client/KeepServerApiClientTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import com.google.common.collect.Maps; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import okio.Buffer; +import org.apache.commons.io.FileUtils; +import org.arvados.client.common.Headers; +import org.arvados.client.test.utils.ArvadosClientMockedWebServerTest; +import org.junit.Test; + +import java.io.File; +import java.util.Map; + +import static org.arvados.client.test.utils.ApiClientTestUtils.assertAuthorizationHeader; +import static org.assertj.core.api.Assertions.assertThat; + +public class KeepServerApiClientTest extends ArvadosClientMockedWebServerTest { + + private KeepServerApiClient client = new KeepServerApiClient(CONFIG); + + @Test + public void uploadFileToServer() throws Exception { + + // given + String blockLocator = "7df44272090cee6c0732382bba415ee9"; + String signedBlockLocator = blockLocator + "+70+A189a93acda6e1fba18a9dffd42b6591cbd36d55d@5a1c17b6"; + server.enqueue(new MockResponse().setBody(signedBlockLocator)); + + String url = server.url(blockLocator).toString(); + File body = new File("README.md"); + Map headers = Maps.newHashMap(); + headers.put(Headers.X_KEEP_DESIRED_REPLICAS, "2"); + + // when + String actual = client.upload(url, headers, body); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertThat(request.getPath()).isEqualTo("/" + blockLocator); + + assertThat(actual).isEqualTo(signedBlockLocator); + } + + @Test + public void downloadFileFromServer() throws Exception { + File data = new File("README.md"); + byte[] fileBytes = FileUtils.readFileToByteArray(data); + server.enqueue(new MockResponse().setBody(new Buffer().write(fileBytes))); + + String blockLocator = "7df44272090cee6c0732382bba415ee9"; + String signedBlockLocator = blockLocator + "+70+A189a93acda6e1fba18a9dffd42b6591cbd36d55d@5a1c17b6"; + + String url = server.url(signedBlockLocator).toString(); + + byte[] actual = client.download(url); + RecordedRequest request = server.takeRequest(); + assertThat(request.getPath()).isEqualTo("/" + signedBlockLocator); + assertThat(actual).isEqualTo(fileBytes); + + } +} diff --git a/src/test/java/org/arvados/client/api/client/KeepServicesApiClientTest.java b/src/test/java/org/arvados/client/api/client/KeepServicesApiClientTest.java new file mode 100644 index 0000000000..015f8328d8 --- /dev/null +++ b/src/test/java/org/arvados/client/api/client/KeepServicesApiClientTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import okhttp3.mockwebserver.RecordedRequest; +import org.arvados.client.api.model.KeepService; +import org.arvados.client.api.model.KeepServiceList; +import org.arvados.client.test.utils.ArvadosClientMockedWebServerTest; +import org.junit.Test; + +import static org.arvados.client.test.utils.ApiClientTestUtils.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class KeepServicesApiClientTest extends ArvadosClientMockedWebServerTest { + + private static final String RESOURCE = "keep_services"; + + private KeepServicesApiClient client = new KeepServicesApiClient(CONFIG); + + @Test + public void listKeepServices() throws Exception { + + // given + server.enqueue(getResponse("keep-services-list")); + + // when + KeepServiceList actual = client.list(); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE); + + assertThat(actual.getItemsAvailable()).isEqualTo(3); + + } + + @Test + public void listAccessibleKeepServices() throws Exception { + + // given + server.enqueue(getResponse("keep-services-accessible")); + + // when + KeepServiceList actual = client.accessible(); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE + "/accessible"); + assertThat(actual.getItemsAvailable()).isEqualTo(2); + } + + @Test + public void getKeepService() throws Exception { + + // given + server.enqueue(getResponse("keep-services-get")); + + String uuid = "112ci-bi6l4-hv02fg8sbti8ykk"; + + // whenFs + KeepService actual = client.get(uuid); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE + "/" + uuid); + assertThat(actual.getUuid()).isEqualTo(uuid); + assertThat(actual.getServiceType()).isEqualTo("disk"); + } + +} diff --git a/src/test/java/org/arvados/client/api/client/UsersApiClientTest.java b/src/test/java/org/arvados/client/api/client/UsersApiClientTest.java new file mode 100644 index 0000000000..40f7bac080 --- /dev/null +++ b/src/test/java/org/arvados/client/api/client/UsersApiClientTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client; + +import okhttp3.mockwebserver.RecordedRequest; +import org.arvados.client.api.model.User; +import org.arvados.client.api.model.UserList; +import org.arvados.client.test.utils.RequestMethod; +import org.arvados.client.test.utils.ArvadosClientMockedWebServerTest; +import org.junit.Test; + +import static org.arvados.client.common.Characters.SLASH; +import static org.arvados.client.test.utils.ApiClientTestUtils.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class UsersApiClientTest extends ArvadosClientMockedWebServerTest { + + private static final String RESOURCE = "users"; + private static final String USER_UUID = "ardev-tpzed-q6dvn7sby55up1b"; + + private UsersApiClient client = new UsersApiClient(CONFIG); + + @Test + public void listUsers() throws Exception { + + // given + server.enqueue(getResponse("users-list")); + + // when + UserList actual = client.list(); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE); + assertRequestMethod(request, RequestMethod.GET); + assertThat(actual.getItemsAvailable()).isEqualTo(13); + } + + @Test + public void getUser() throws Exception { + + // given + server.enqueue(getResponse("users-get")); + + // when + User actual = client.get(USER_UUID); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE + SLASH + USER_UUID); + assertRequestMethod(request, RequestMethod.GET); + assertThat(actual.getUuid()).isEqualTo(USER_UUID); + } + + @Test + public void getCurrentUser() throws Exception { + + // given + server.enqueue(getResponse("users-get")); + + // when + User actual = client.current(); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE + SLASH + "current"); + assertRequestMethod(request, RequestMethod.GET); + assertThat(actual.getUuid()).isEqualTo(USER_UUID); + } + + @Test + public void getSystemUser() throws Exception { + + // given + server.enqueue(getResponse("users-system")); + + // when + User actual = client.system(); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE + SLASH + "system"); + assertRequestMethod(request, RequestMethod.GET); + assertThat(actual.getUuid()).isEqualTo("ardev-tpzed-000000000000000"); + } + + @Test + public void createUser() throws Exception { + + // given + server.enqueue(getResponse("users-create")); + + String firstName = "John"; + String lastName = "Wayne"; + String fullName = String.format("%s %s", firstName, lastName); + String username = String.format("%s%s", firstName, lastName).toLowerCase(); + + User user = new User(); + user.setFirstName(firstName); + user.setLastName(lastName); + user.setFullName(fullName); + user.setUsername(username); + + // when + User actual = client.create(user); + + // then + RecordedRequest request = server.takeRequest(); + assertAuthorizationHeader(request); + assertRequestPath(request, RESOURCE); + assertRequestMethod(request, RequestMethod.POST); + assertThat(actual.getFirstName()).isEqualTo(firstName); + assertThat(actual.getLastName()).isEqualTo(lastName); + assertThat(actual.getFullName()).isEqualTo(fullName); + assertThat(actual.getUsername()).isEqualTo(username); + } +} diff --git a/src/test/java/org/arvados/client/api/client/factory/OkHttpClientFactoryTest.java b/src/test/java/org/arvados/client/api/client/factory/OkHttpClientFactoryTest.java new file mode 100644 index 0000000000..f7e1813294 --- /dev/null +++ b/src/test/java/org/arvados/client/api/client/factory/OkHttpClientFactoryTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.api.client.factory; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.mockwebserver.MockResponse; +import org.arvados.client.test.utils.ArvadosClientMockedWebServerTest; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import java.io.FileInputStream; +import java.security.KeyStore; + + +@RunWith(MockitoJUnitRunner.class) +public class OkHttpClientFactoryTest extends ArvadosClientMockedWebServerTest { + + @Test(expected = javax.net.ssl.SSLHandshakeException.class) + public void secureOkHttpClientIsCreated() throws Exception { + + // given + OkHttpClientFactory factory = OkHttpClientFactory.builder().build(); + // * configure HTTPS server + SSLSocketFactory sf = getSSLSocketFactoryWithSelfSignedCertificate(); + server.useHttps(sf, false); + server.enqueue(new MockResponse().setBody("OK")); + // * prepare client HTTP request + Request request = new Request.Builder() + .url("https://localhost:9000/") + .build(); + + // when - then (SSL certificate is verified) + OkHttpClient actual = factory.create(false); + Response response = actual.newCall(request).execute(); + } + + @Test + public void insecureOkHttpClientIsCreated() throws Exception { + // given + OkHttpClientFactory factory = OkHttpClientFactory.builder().build(); + // * configure HTTPS server + SSLSocketFactory sf = getSSLSocketFactoryWithSelfSignedCertificate(); + server.useHttps(sf, false); + server.enqueue(new MockResponse().setBody("OK")); + // * prepare client HTTP request + Request request = new Request.Builder() + .url("https://localhost:9000/") + .build(); + + // when (SSL certificate is not verified) + OkHttpClient actual = factory.create(true); + Response response = actual.newCall(request).execute(); + + // then + Assert.assertEquals(response.body().string(),"OK"); + } + + + /* + This ugly boilerplate is needed to enable self signed certificate. + + It requires selfsigned.keystore.jks file. It was generated with: + keytool -genkey -v -keystore mystore.keystore.jks -alias alias_name -keyalg RSA -keysize 2048 -validity 10000 + */ + public SSLSocketFactory getSSLSocketFactoryWithSelfSignedCertificate() throws Exception { + + FileInputStream stream = new FileInputStream("src/test/resources/selfsigned.keystore.jks"); + char[] serverKeyStorePassword = "123456".toCharArray(); + KeyStore serverKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + serverKeyStore.load(stream, serverKeyStorePassword); + + String kmfAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgorithm); + kmf.init(serverKeyStore, serverKeyStorePassword); + + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(kmfAlgorithm); + trustManagerFactory.init(serverKeyStore); + + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(kmf.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); + return sslContext.getSocketFactory(); + } +} diff --git a/src/test/java/org/arvados/client/facade/ArvadosFacadeIntegrationTest.java b/src/test/java/org/arvados/client/facade/ArvadosFacadeIntegrationTest.java new file mode 100644 index 0000000000..07269f7e7d --- /dev/null +++ b/src/test/java/org/arvados/client/facade/ArvadosFacadeIntegrationTest.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.facade; + +import org.apache.commons.io.FileUtils; +import org.arvados.client.api.model.Collection; +import org.arvados.client.common.Characters; +import org.arvados.client.config.ExternalConfigProvider; +import org.arvados.client.junit.categories.IntegrationTests; +import org.arvados.client.logic.collection.FileToken; +import org.arvados.client.test.utils.ArvadosClientIntegrationTest; +import org.arvados.client.test.utils.FileTestUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import java.io.File; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.arvados.client.test.utils.FileTestUtils.FILE_DOWNLOAD_TEST_DIR; +import static org.arvados.client.test.utils.FileTestUtils.FILE_SPLIT_TEST_DIR; +import static org.arvados.client.test.utils.FileTestUtils.TEST_FILE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@Category(IntegrationTests.class) +public class ArvadosFacadeIntegrationTest extends ArvadosClientIntegrationTest { + + + private static final String COLLECTION_NAME = "Test collection " + UUID.randomUUID().toString(); + private String collectionUuid; + + @Before + public void setUp() throws Exception { + FileTestUtils.createDirectory(FILE_SPLIT_TEST_DIR); + FileTestUtils.createDirectory(FILE_DOWNLOAD_TEST_DIR); + } + + @Test + public void uploadOfFileIsPerformedSuccessfully() throws Exception { + // given + File file = FileTestUtils.generateFile(TEST_FILE, FileTestUtils.ONE_FOURTH_GB / 200); + + // when + Collection actual = FACADE.upload(Collections.singletonList(file), COLLECTION_NAME, PROJECT_UUID); + collectionUuid = actual.getUuid(); + + // then + assertThat(actual.getName()).contains("Test collection"); + assertThat(actual.getManifestText()).contains(file.length() + Characters.COLON + file.getName()); + } + + @Test + public void uploadOfFilesIsPerformedSuccessfully() throws Exception { + // given + List files = FileTestUtils.generatePredefinedFiles(); + files.addAll(FileTestUtils.generatePredefinedFiles()); + + // when + Collection actual = FACADE.upload(files, COLLECTION_NAME, PROJECT_UUID); + collectionUuid = actual.getUuid(); + + // then + assertThat(actual.getName()).contains("Test collection"); + files.forEach(f -> assertThat(actual.getManifestText()).contains(f.length() + Characters.COLON + f.getName().replace(" ", Characters.SPACE))); + } + + @Test + public void uploadToExistingCollectionIsPerformedSuccessfully() throws Exception { + // given + File file = FileTestUtils.generateFile(TEST_FILE, FileTestUtils.ONE_EIGTH_GB / 500); + Collection existing = createTestCollection(); + + // when + Collection actual = FACADE.uploadToExistingCollection(Collections.singletonList(file), collectionUuid); + + // then + assertEquals(collectionUuid, actual.getUuid()); + assertThat(actual.getManifestText()).contains(file.length() + Characters.COLON + file.getName()); + } + + @Test + public void uploadWithExternalConfigProviderWorksProperly() throws Exception { + //given + ArvadosFacade facade = new ArvadosFacade(buildExternalConfig()); + File file = FileTestUtils.generateFile(TEST_FILE, FileTestUtils.ONE_FOURTH_GB / 200); + + //when + Collection actual = facade.upload(Collections.singletonList(file), COLLECTION_NAME, PROJECT_UUID); + collectionUuid = actual.getUuid(); + + //then + assertThat(actual.getName()).contains("Test collection"); + assertThat(actual.getManifestText()).contains(file.length() + Characters.COLON + file.getName()); + } + + @Test + public void creationOfEmptyCollectionPerformedSuccesfully() { + // given + String collectionName = "Empty collection " + UUID.randomUUID().toString(); + + // when + Collection actual = FACADE.createEmptyCollection(collectionName, PROJECT_UUID); + collectionUuid = actual.getUuid(); + + // then + assertEquals(collectionName, actual.getName()); + assertEquals(PROJECT_UUID, actual.getOwnerUuid()); + } + + @Test + public void fileTokensAreListedFromCollection() throws Exception { + //given + List files = uploadTestFiles(); + + //when + List actual = FACADE.listFileInfoFromCollection(collectionUuid); + + //then + assertEquals(files.size(), actual.size()); + for (int i = 0; i < files.size(); i++) { + assertEquals(files.get(i).length(), actual.get(i).getFileSize()); + } + } + + @Test + public void downloadOfFilesPerformedSuccessfully() throws Exception { + //given + List files = uploadTestFiles(); + File destination = new File(FILE_DOWNLOAD_TEST_DIR + Characters.SLASH + collectionUuid); + + //when + List actual = FACADE.downloadCollectionFiles(collectionUuid, FILE_DOWNLOAD_TEST_DIR, false); + + //then + assertEquals(files.size(), actual.size()); + assertTrue(destination.exists()); + assertThat(actual).allMatch(File::exists); + for (int i = 0; i < files.size(); i++) { + assertEquals(files.get(i).length(), actual.get(i).length()); + } + } + + @Test + public void downloadOfFilesPerformedSuccessfullyUsingKeepWeb() throws Exception { + //given + List files = uploadTestFiles(); + File destination = new File(FILE_DOWNLOAD_TEST_DIR + Characters.SLASH + collectionUuid); + + //when + List actual = FACADE.downloadCollectionFiles(collectionUuid, FILE_DOWNLOAD_TEST_DIR, true); + + //then + assertEquals(files.size(), actual.size()); + assertTrue(destination.exists()); + assertThat(actual).allMatch(File::exists); + for (int i = 0; i < files.size(); i++) { + assertEquals(files.get(i).length(), actual.get(i).length()); + } + } + + @Test + public void singleFileIsDownloadedSuccessfullyUsingKeepWeb() throws Exception { + //given + File file = uploadSingleTestFile(false); + + //when + File actual = FACADE.downloadFile(file.getName(), collectionUuid, FILE_DOWNLOAD_TEST_DIR); + + //then + assertThat(actual).exists(); + assertThat(actual.length()).isEqualTo(file.length()); + } + + @Test + public void downloadOfOneFileSplittedToMultipleLocatorsPerformedSuccesfully() throws Exception { + //given + File file = uploadSingleTestFile(true); + + List actual = FACADE.downloadCollectionFiles(collectionUuid, FILE_DOWNLOAD_TEST_DIR, false); + + Assert.assertEquals(1, actual.size()); + assertThat(actual.get(0).length()).isEqualTo(file.length()); + } + + @Test + public void downloadWithExternalConfigProviderWorksProperly() throws Exception { + //given + ArvadosFacade facade = new ArvadosFacade(buildExternalConfig()); + List files = uploadTestFiles(); + //when + List actual = facade.downloadCollectionFiles(collectionUuid, FILE_DOWNLOAD_TEST_DIR, false); + + //then + assertEquals(files.size(), actual.size()); + assertThat(actual).allMatch(File::exists); + for (int i = 0; i < files.size(); i++) { + assertEquals(files.get(i).length(), actual.get(i).length()); + } + } + + private ExternalConfigProvider buildExternalConfig() { + return ExternalConfigProvider + .builder() + .apiHostInsecure(CONFIG.isApiHostInsecure()) + .keepWebHost(CONFIG.getKeepWebHost()) + .keepWebPort(CONFIG.getKeepWebPort()) + .apiHost(CONFIG.getApiHost()) + .apiPort(CONFIG.getApiPort()) + .apiToken(CONFIG.getApiToken()) + .apiProtocol(CONFIG.getApiProtocol()) + .fileSplitSize(CONFIG.getFileSplitSize()) + .fileSplitDirectory(CONFIG.getFileSplitDirectory()) + .numberOfCopies(CONFIG.getNumberOfCopies()) + .numberOfRetries(CONFIG.getNumberOfRetries()) + .build(); + } + + private Collection createTestCollection() { + Collection collection = FACADE.createEmptyCollection(COLLECTION_NAME, PROJECT_UUID); + collectionUuid = collection.getUuid(); + return collection; + } + + private List uploadTestFiles() throws Exception{ + createTestCollection(); + List files = FileTestUtils.generatePredefinedFiles(); + FACADE.uploadToExistingCollection(files, collectionUuid); + return files; + } + + private File uploadSingleTestFile(boolean bigFile) throws Exception{ + createTestCollection(); + Long fileSize = bigFile ? FileUtils.ONE_MB * 70 : FileTestUtils.ONE_EIGTH_GB / 100; + File file = FileTestUtils.generateFile(TEST_FILE, fileSize); + FACADE.uploadToExistingCollection(Collections.singletonList(file), collectionUuid); + return file; + } + + @After + public void tearDown() throws Exception { + FileTestUtils.cleanDirectory(FILE_SPLIT_TEST_DIR); + FileTestUtils.cleanDirectory(FILE_DOWNLOAD_TEST_DIR); + + if(collectionUuid != null) + FACADE.deleteCollection(collectionUuid); + } +} diff --git a/src/test/java/org/arvados/client/facade/ArvadosFacadeTest.java b/src/test/java/org/arvados/client/facade/ArvadosFacadeTest.java new file mode 100644 index 0000000000..a025011d79 --- /dev/null +++ b/src/test/java/org/arvados/client/facade/ArvadosFacadeTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.facade; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import okhttp3.mockwebserver.MockResponse; +import okio.Buffer; +import org.apache.commons.io.FileUtils; +import org.arvados.client.api.model.Collection; +import org.arvados.client.api.model.KeepService; +import org.arvados.client.api.model.KeepServiceList; +import org.arvados.client.common.Characters; +import org.arvados.client.test.utils.ArvadosClientMockedWebServerTest; +import org.arvados.client.test.utils.FileTestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.arvados.client.test.utils.ApiClientTestUtils.getResponse; +import static org.arvados.client.test.utils.FileTestUtils.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class ArvadosFacadeTest extends ArvadosClientMockedWebServerTest { + + ArvadosFacade facade = new ArvadosFacade(CONFIG); + + @Before + public void setUp() throws Exception { + FileTestUtils.createDirectory(FILE_SPLIT_TEST_DIR); + FileTestUtils.createDirectory(FILE_DOWNLOAD_TEST_DIR); + } + + @Test + public void uploadIsPerformedSuccessfullyUsingDiskOnlyKeepServices() throws Exception { + + // given + String keepServicesAccessible = setMockedServerPortToKeepServices("keep-services-accessible-disk-only"); + server.enqueue(new MockResponse().setBody(keepServicesAccessible)); + + String blockLocator = "7df44272090cee6c0732382bba415ee9"; + String signedBlockLocator = blockLocator + "+70+A189a93acda6e1fba18a9dffd42b6591cbd36d55d@5a1c17b6"; + for (int i = 0; i < 8; i++) { + server.enqueue(new MockResponse().setBody(signedBlockLocator)); + } + server.enqueue(getResponse("users-get")); + server.enqueue(getResponse("collections-create-manifest")); + + FileTestUtils.generateFile(TEST_FILE, FileTestUtils.ONE_FOURTH_GB); + + // when + Collection actual = facade.upload(Arrays.asList(new File(TEST_FILE)), "Super Collection", null); + + // then + assertThat(actual.getName()).contains("Super Collection"); + } + + @Test + public void uploadIsPerformedSuccessfully() throws Exception { + + // given + String keepServicesAccessible = setMockedServerPortToKeepServices("keep-services-accessible"); + server.enqueue(new MockResponse().setBody(keepServicesAccessible)); + + String blockLocator = "7df44272090cee6c0732382bba415ee9"; + String signedBlockLocator = blockLocator + "+70+A189a93acda6e1fba18a9dffd42b6591cbd36d55d@5a1c17b6"; + for (int i = 0; i < 4; i++) { + server.enqueue(new MockResponse().setBody(signedBlockLocator)); + } + server.enqueue(getResponse("users-get")); + server.enqueue(getResponse("collections-create-manifest")); + + FileTestUtils.generateFile(TEST_FILE, FileTestUtils.ONE_FOURTH_GB); + + // when + Collection actual = facade.upload(Arrays.asList(new File(TEST_FILE)), "Super Collection", null); + + // then + assertThat(actual.getName()).contains("Super Collection"); + } + + @Test + public void downloadOfWholeCollectionIsPerformedSuccessfully() throws Exception { + + //given + String collectionUuid = "ardev-4zz18-jk5vo4uo9u5vj52"; + server.enqueue(getResponse("collections-download-file")); + + String keepServicesAccessible = setMockedServerPortToKeepServices("keep-services-accessible"); + server.enqueue(new MockResponse().setBody(keepServicesAccessible)); + File collectionDestination = new File(FILE_DOWNLOAD_TEST_DIR + Characters.SLASH + collectionUuid); + + List files = generatePredefinedFiles(); + List fileData = new ArrayList<>(); + for (File f : files) { + fileData.add(Files.readAllBytes(f.toPath())); + } + byte[] filesDataChunk = fileData.stream().reduce(new byte[0], this::addAll); + + server.enqueue(new MockResponse().setBody(new Buffer().write(filesDataChunk))); + + //when + List downloadedFiles = facade.downloadCollectionFiles(collectionUuid, FILE_DOWNLOAD_TEST_DIR, false); + + //then + assertEquals(3, downloadedFiles.size()); + assertTrue(collectionDestination.exists()); + assertThat(downloadedFiles).allMatch(File::exists); + assertEquals(files.stream().map(File::getName).collect(Collectors.toList()), downloadedFiles.stream().map(File::getName).collect(Collectors.toList())); + assertEquals(files.stream().map(File::length).collect(Collectors.toList()), downloadedFiles.stream().map(File::length).collect(Collectors.toList())); + } + + @Test + public void downloadOfWholeCollectionUsingKeepWebPerformedSuccessfully() throws Exception { + + //given + String collectionUuid = "ardev-4zz18-jk5vo4uo9u5vj52"; + server.enqueue(getResponse("collections-download-file")); + + List files = generatePredefinedFiles(); + for (File f : files) { + server.enqueue(new MockResponse().setBody(new Buffer().write(FileUtils.readFileToByteArray(f)))); + } + + //when + List downloadedFiles = facade.downloadCollectionFiles(collectionUuid, FILE_DOWNLOAD_TEST_DIR, true); + + //then + assertEquals(3, downloadedFiles.size()); + assertThat(downloadedFiles).allMatch(File::exists); + assertEquals(files.stream().map(File::getName).collect(Collectors.toList()), downloadedFiles.stream().map(File::getName).collect(Collectors.toList())); + assertTrue(downloadedFiles.stream().map(File::length).collect(Collectors.toList()).containsAll(files.stream().map(File::length).collect(Collectors.toList()))); + } + + @Test + public void downloadOfSingleFilePerformedSuccessfully() throws Exception { + + //given + String collectionUuid = "ardev-4zz18-jk5vo4uo9u5vj52"; + server.enqueue(getResponse("collections-download-file")); + + File file = generatePredefinedFiles().get(0); + byte[] fileData = FileUtils.readFileToByteArray(file); + server.enqueue(new MockResponse().setBody(new Buffer().write(fileData))); + + //when + File downloadedFile = facade.downloadFile(file.getName(), collectionUuid, FILE_DOWNLOAD_TEST_DIR); + + //then + assertTrue(downloadedFile.exists()); + assertEquals(file.getName(), downloadedFile.getName()); + assertEquals(file.length(), downloadedFile.length()); + } + + private String setMockedServerPortToKeepServices(String jsonPath) throws Exception { + + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + String filePath = String.format("src/test/resources/org/arvados/client/api/client/%s.json", jsonPath); + File jsonFile = new File(filePath); + String json = FileUtils.readFileToString(jsonFile, Charset.defaultCharset()); + KeepServiceList keepServiceList = mapper.readValue(json, KeepServiceList.class); + List items = keepServiceList.getItems(); + for (KeepService keepService : items) { + keepService.setServicePort(server.getPort()); + } + ObjectWriter writer = mapper.writer().withDefaultPrettyPrinter(); + return writer.writeValueAsString(keepServiceList); + } + + //Method to copy multiple byte[] arrays into one byte[] array + private byte[] addAll(byte[] array1, byte[] array2) { + byte[] joinedArray = new byte[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + @After + public void tearDown() throws Exception { + FileTestUtils.cleanDirectory(FILE_SPLIT_TEST_DIR); + FileTestUtils.cleanDirectory(FILE_DOWNLOAD_TEST_DIR); + } +} diff --git a/src/test/java/org/arvados/client/junit/categories/IntegrationTests.java b/src/test/java/org/arvados/client/junit/categories/IntegrationTests.java new file mode 100644 index 0000000000..6a0e78d661 --- /dev/null +++ b/src/test/java/org/arvados/client/junit/categories/IntegrationTests.java @@ -0,0 +1,10 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.junit.categories; + +public interface IntegrationTests {} diff --git a/src/test/java/org/arvados/client/logic/collection/FileTokenTest.java b/src/test/java/org/arvados/client/logic/collection/FileTokenTest.java new file mode 100644 index 0000000000..13939852cb --- /dev/null +++ b/src/test/java/org/arvados/client/logic/collection/FileTokenTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.collection; + +import org.arvados.client.common.Characters; +import org.junit.Assert; +import org.junit.Test; + +public class FileTokenTest { + + public static final String FILE_TOKEN_INFO = "0:1024:test-file1"; + public static final int FILE_POSITION = 0; + public static final int FILE_LENGTH = 1024; + public static final String FILE_NAME = "test-file1"; + public static final String FILE_PATH = "c" + Characters.SLASH; + + private static FileToken fileToken = new FileToken(FILE_TOKEN_INFO); + private static FileToken fileTokenWithPath = new FileToken(FILE_TOKEN_INFO, FILE_PATH); + + @Test + public void tokenInfoIsDividedCorrectly(){ + Assert.assertEquals(FILE_NAME, fileToken.getFileName()); + Assert.assertEquals(FILE_POSITION, fileToken.getFilePosition()); + Assert.assertEquals(FILE_LENGTH, fileToken.getFileSize()); + } + + @Test + public void toStringReturnsOriginalFileTokenInfo(){ + Assert.assertEquals(FILE_TOKEN_INFO, fileToken.toString()); + } + + @Test + public void fullPathIsReturnedProperly(){ + Assert.assertEquals(FILE_NAME, fileToken.getFullPath()); + Assert.assertEquals(FILE_PATH + FILE_NAME, fileTokenWithPath.getFullPath()); + } +} diff --git a/src/test/java/org/arvados/client/logic/collection/ManifestDecoderTest.java b/src/test/java/org/arvados/client/logic/collection/ManifestDecoderTest.java new file mode 100644 index 0000000000..c9464e03b6 --- /dev/null +++ b/src/test/java/org/arvados/client/logic/collection/ManifestDecoderTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.collection; + +import org.arvados.client.exception.ArvadosClientException; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +import static junit.framework.TestCase.fail; + +public class ManifestDecoderTest { + + private ManifestDecoder manifestDecoder = new ManifestDecoder(); + + private static final String ONE_LINE_MANIFEST_TEXT = ". " + + "eff999f3b5158331eb44a9a93e3b36e1+67108864+Aad3839bea88bce22cbfe71cf4943de7dab3ea52a@5826180f " + + "db141bfd11f7da60dce9e5ee85a988b8+34038725+Ae8f48913fed782cbe463e0499ab37697ee06a2f8@5826180f " + + "0:101147589:rna.SRR948778.bam" + + "\\n"; + + private static final String MULTIPLE_LINES_MANIFEST_TEXT = ". " + + "930625b054ce894ac40596c3f5a0d947+33 " + + "0:0:a 0:0:b 0:33:output.txt\n" + + "./c d41d8cd98f00b204e9800998ecf8427e+0 0:0:d"; + + private static final String MANIFEST_TEXT_WITH_INVALID_FIRST_PATH_COMPONENT = "a" + ONE_LINE_MANIFEST_TEXT; + + + @Test + public void allLocatorsAndFileTokensAreExtractedFromSimpleManifest() { + + List actual = manifestDecoder.decode(ONE_LINE_MANIFEST_TEXT); + + // one manifest stream + Assert.assertEquals(1, actual.size()); + + ManifestStream manifest = actual.get(0); + // two locators + Assert.assertEquals(2, manifest.getKeepLocators().size()); + // one file token + Assert.assertEquals(1, manifest.getFileTokens().size()); + + } + + @Test + public void allLocatorsAndFileTokensAreExtractedFromComplexManifest() { + + List actual = manifestDecoder.decode(MULTIPLE_LINES_MANIFEST_TEXT); + + // two manifest streams + Assert.assertEquals(2, actual.size()); + + // first stream - 1 locator and 3 file tokens + ManifestStream firstManifestStream = actual.get(0); + Assert.assertEquals(1, firstManifestStream.getKeepLocators().size()); + Assert.assertEquals(3, firstManifestStream.getFileTokens().size()); + + // second stream - 1 locator and 1 file token + ManifestStream secondManifestStream = actual.get(1); + Assert.assertEquals(1, secondManifestStream.getKeepLocators().size()); + Assert.assertEquals(1, secondManifestStream.getFileTokens().size()); + } + + @Test + public void manifestTextWithInvalidStreamNameThrowsException() { + + try { + List actual = manifestDecoder.decode(MANIFEST_TEXT_WITH_INVALID_FIRST_PATH_COMPONENT); + fail(); + } catch (ArvadosClientException e) { + Assert.assertEquals("Invalid first path component (expecting \".\")", e.getMessage()); + } + + } + + @Test + public void emptyManifestTextThrowsException() { + String emptyManifestText = null; + + try { + List actual = manifestDecoder.decode(emptyManifestText); + fail(); + } catch (ArvadosClientException e) { + Assert.assertEquals("Manifest text cannot be empty.", e.getMessage()); + } + + emptyManifestText = ""; + try { + List actual = manifestDecoder.decode(emptyManifestText); + fail(); + } catch (ArvadosClientException e) { + Assert.assertEquals("Manifest text cannot be empty.", e.getMessage()); + } + + } + + + + + +} diff --git a/src/test/java/org/arvados/client/logic/collection/ManifestFactoryTest.java b/src/test/java/org/arvados/client/logic/collection/ManifestFactoryTest.java new file mode 100644 index 0000000000..06ed07d8ed --- /dev/null +++ b/src/test/java/org/arvados/client/logic/collection/ManifestFactoryTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.collection; + +import org.arvados.client.test.utils.FileTestUtils; +import org.assertj.core.util.Lists; +import org.junit.Test; + +import java.io.File; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ManifestFactoryTest { + + @Test + public void manifestIsCreatedAsExpected() throws Exception { + + // given + List files = FileTestUtils.generatePredefinedFiles(); + List locators = Lists.newArrayList("a", "b", "c"); + ManifestFactory factory = ManifestFactory.builder() + .files(files) + .locators(locators) + .build(); + + // when + String actual = factory.create(); + + // then + assertThat(actual).isEqualTo(". a b c 0:1024:test-file1 1024:20480:test-file2 21504:1048576:test-file\\0403\n"); + } +} diff --git a/src/test/java/org/arvados/client/logic/collection/ManifestStreamTest.java b/src/test/java/org/arvados/client/logic/collection/ManifestStreamTest.java new file mode 100644 index 0000000000..bc36889f71 --- /dev/null +++ b/src/test/java/org/arvados/client/logic/collection/ManifestStreamTest.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.collection; + + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class ManifestStreamTest { + + private ManifestDecoder manifestDecoder = new ManifestDecoder(); + + @Test + public void toStringReturnsProperlyConnectedManifestStream() throws Exception{ + String encodedManifest = ". eff999f3b5158331eb44a9a93e3b36e1+67108864 db141bfd11f7da60dce9e5ee85a988b8+34038725 0:101147589:rna.SRR948778.bam\\n\""; + List manifestStreams = manifestDecoder.decode(encodedManifest); + Assert.assertEquals(encodedManifest, manifestStreams.get(0).toString()); + + } +} diff --git a/src/test/java/org/arvados/client/logic/keep/FileDownloaderTest.java b/src/test/java/org/arvados/client/logic/keep/FileDownloaderTest.java new file mode 100644 index 0000000000..0fb1f0206c --- /dev/null +++ b/src/test/java/org/arvados/client/logic/keep/FileDownloaderTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.keep; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.arvados.client.api.client.CollectionsApiClient; +import org.arvados.client.api.client.KeepWebApiClient; +import org.arvados.client.api.model.Collection; +import org.arvados.client.common.Characters; +import org.arvados.client.logic.collection.FileToken; +import org.arvados.client.logic.collection.ManifestDecoder; +import org.arvados.client.logic.collection.ManifestStream; +import org.arvados.client.test.utils.FileTestUtils; +import org.arvados.client.utils.FileMerge; +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.arvados.client.test.utils.FileTestUtils.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class FileDownloaderTest { + + static final ObjectMapper MAPPER = new ObjectMapper().findAndRegisterModules(); + private Collection collectionToDownload; + private ManifestStream manifestStream; + + @Mock + private CollectionsApiClient collectionsApiClient; + @Mock + private KeepClient keepClient; + @Mock + private KeepWebApiClient keepWebApiClient; + @Mock + private ManifestDecoder manifestDecoder; + @InjectMocks + private FileDownloader fileDownloader; + + @Before + public void setUp() throws Exception { + FileTestUtils.createDirectory(FILE_SPLIT_TEST_DIR); + FileTestUtils.createDirectory(FILE_DOWNLOAD_TEST_DIR); + + collectionToDownload = prepareCollection(); + manifestStream = prepareManifestStream(); + } + + @Test + public void downloadingAllFilesFromCollectionWorksProperly() throws Exception { + // given + List files = generatePredefinedFiles(); + byte[] dataChunk = prepareDataChunk(files); + + //having + when(collectionsApiClient.get(collectionToDownload.getUuid())).thenReturn(collectionToDownload); + when(manifestDecoder.decode(collectionToDownload.getManifestText())).thenReturn(Arrays.asList(manifestStream)); + when(keepClient.getDataChunk(manifestStream.getKeepLocators().get(0))).thenReturn(dataChunk); + + //when + List downloadedFiles = fileDownloader.downloadFilesFromCollection(collectionToDownload.getUuid(), FILE_DOWNLOAD_TEST_DIR); + + //then + Assert.assertEquals(3, downloadedFiles.size()); // 3 files downloaded + + File collectionDir = new File(FILE_DOWNLOAD_TEST_DIR + Characters.SLASH + collectionToDownload.getUuid()); + Assert.assertTrue(collectionDir.exists()); // collection directory created + + // 3 files correctly saved + assertThat(downloadedFiles).allMatch(File::exists); + + for(int i = 0; i < downloadedFiles.size(); i ++) { + File downloaded = new File(collectionDir + Characters.SLASH + files.get(i).getName()); + Assert.assertArrayEquals(FileUtils.readFileToByteArray(downloaded), FileUtils.readFileToByteArray(files.get(i))); + } + } + + @Test + public void downloadingSingleFileFromKeepWebWorksCorrectly() throws Exception{ + //given + File file = generatePredefinedFiles().get(0); + + //having + when(collectionsApiClient.get(collectionToDownload.getUuid())).thenReturn(collectionToDownload); + when(manifestDecoder.decode(collectionToDownload.getManifestText())).thenReturn(Arrays.asList(manifestStream)); + when(keepWebApiClient.download(collectionToDownload.getUuid(), file.getName())).thenReturn(FileUtils.readFileToByteArray(file)); + + //when + File downloadedFile = fileDownloader.downloadSingleFileUsingKeepWeb(file.getName(), collectionToDownload.getUuid(), FILE_DOWNLOAD_TEST_DIR); + + //then + Assert.assertTrue(downloadedFile.exists()); + Assert.assertEquals(file.getName(), downloadedFile.getName()); + Assert.assertArrayEquals(FileUtils.readFileToByteArray(downloadedFile), FileUtils.readFileToByteArray(file)); + } + + @After + public void tearDown() throws Exception { + FileTestUtils.cleanDirectory(FILE_SPLIT_TEST_DIR); + FileTestUtils.cleanDirectory(FILE_DOWNLOAD_TEST_DIR); + } + + private Collection prepareCollection() throws IOException { + // collection that will be returned by mocked collectionsApiClient + String filePath = "src/test/resources/org/arvados/client/api/client/collections-download-file.json"; + File jsonFile = new File(filePath); + return MAPPER.readValue(jsonFile, Collection.class); + } + + private ManifestStream prepareManifestStream() throws Exception { + // manifestStream that will be returned by mocked manifestDecoder + List fileTokens = new ArrayList<>(); + fileTokens.add(new FileToken("0:1024:test-file1")); + fileTokens.add(new FileToken("1024:20480:test-file2")); + fileTokens.add(new FileToken("21504:1048576:test-file\\0403")); + + KeepLocator keepLocator = new KeepLocator("163679d58edaadc28db769011728a72c+1070080+A3acf8c1fe582c265d2077702e4a7d74fcc03aba8@5aa4fdeb"); + return new ManifestStream(".", Arrays.asList(keepLocator), fileTokens); + } + + private byte[] prepareDataChunk(List files) throws IOException { + File combinedFile = new File(FILE_SPLIT_TEST_DIR + Characters.SLASH + UUID.randomUUID()); + FileMerge.merge(files, combinedFile); + return FileUtils.readFileToByteArray(combinedFile); + } +} diff --git a/src/test/java/org/arvados/client/logic/keep/KeepClientTest.java b/src/test/java/org/arvados/client/logic/keep/KeepClientTest.java new file mode 100644 index 0000000000..e4e7bf2720 --- /dev/null +++ b/src/test/java/org/arvados/client/logic/keep/KeepClientTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.keep; + +import okhttp3.mockwebserver.MockResponse; +import okio.Buffer; +import org.apache.commons.io.FileUtils; +import org.arvados.client.config.FileConfigProvider; +import org.arvados.client.config.ConfigProvider; +import org.arvados.client.exception.ArvadosClientException; +import org.arvados.client.test.utils.ArvadosClientMockedWebServerTest; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.File; + +import static junit.framework.TestCase.fail; +import static org.arvados.client.test.utils.ApiClientTestUtils.getResponse; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class KeepClientTest extends ArvadosClientMockedWebServerTest { + + private ConfigProvider configProvider = new FileConfigProvider(); + private static final String TEST_FILE_PATH ="src/test/resources/org/arvados/client/api/client/keep-client-test-file.txt"; + + @InjectMocks + private KeepClient keepClient = new KeepClient(configProvider); + + @Mock + private KeepLocator keepLocator; + + @Test + public void uploadedFile() throws Exception { + // given + server.enqueue(getResponse("keep-services-accessible")); + server.enqueue(new MockResponse().setBody("0887c78c7d6c1a60ac0b3709a4302ee4")); + + // when + String actual = keepClient.put(new File(TEST_FILE_PATH), 1, 0); + + // then + assertThat(actual).isEqualTo("0887c78c7d6c1a60ac0b3709a4302ee4"); + } + + @Test + public void fileIsDownloaded() throws Exception { + //given + File data = new File(TEST_FILE_PATH); + byte[] fileBytes = FileUtils.readFileToByteArray(data); + + // when + server.enqueue(getResponse("keep-services-accessible")); + server.enqueue(new MockResponse().setBody(new Buffer().write(fileBytes))); + + byte[] actual = keepClient.getDataChunk(keepLocator); + + Assert.assertArrayEquals(fileBytes, actual); + } + + @Test + public void fileIsDownloadedWhenFirstServerDoesNotRespond() throws Exception { + // given + File data = new File(TEST_FILE_PATH); + byte[] fileBytes = FileUtils.readFileToByteArray(data); + server.enqueue(getResponse("keep-services-accessible")); // two servers accessible + server.enqueue(new MockResponse().setResponseCode(404)); // first one not responding + server.enqueue(new MockResponse().setBody(new Buffer().write(fileBytes))); // second one responding + + //when + byte[] actual = keepClient.getDataChunk(keepLocator); + + //then + Assert.assertArrayEquals(fileBytes, actual); + } + + @Test + public void exceptionIsThrownWhenNoServerResponds() throws Exception { + //given + File data = new File(TEST_FILE_PATH); + server.enqueue(getResponse("keep-services-accessible")); // two servers accessible + server.enqueue(new MockResponse().setResponseCode(404)); // first one not responding + server.enqueue(new MockResponse().setResponseCode(404)); // second one not responding + + try { + //when + keepClient.getDataChunk(keepLocator); + fail(); + } catch (ArvadosClientException e) { + //then + Assert.assertEquals("No server responding. Unable to download data chunk.", e.getMessage()); + } + } + + @Test + public void exceptionIsThrownWhenThereAreNoServersAccessible() throws Exception { + //given + server.enqueue(getResponse("keep-services-not-accessible")); // no servers accessible + + try { + //when + keepClient.getDataChunk(keepLocator); + fail(); + } catch (ArvadosClientException e) { + //then + Assert.assertEquals("No gateway services available!", e.getMessage()); + } + } +} diff --git a/src/test/java/org/arvados/client/logic/keep/KeepLocatorTest.java b/src/test/java/org/arvados/client/logic/keep/KeepLocatorTest.java new file mode 100644 index 0000000000..c4c48da7c5 --- /dev/null +++ b/src/test/java/org/arvados/client/logic/keep/KeepLocatorTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.logic.keep; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KeepLocatorTest { + + private KeepLocator locator; + + @Test + public void md5sumIsExtracted() throws Exception { + + // given + locator = new KeepLocator("7df44272090cee6c0732382bba415ee9+70"); + + // when + String actual = locator.getMd5sum(); + + // then + assertThat(actual).isEqualTo("7df44272090cee6c0732382bba415ee9"); + } + + @Test + public void locatorIsStrippedWithMd5sumAndSize() throws Exception { + + // given + locator = new KeepLocator("7df44272090cee6c0732382bba415ee9+70"); + + // when + String actual = locator.stripped(); + + // then + assertThat(actual).isEqualTo("7df44272090cee6c0732382bba415ee9+70"); + } + + + @Test + public void locatorToStringProperlyShowing() throws Exception { + + // given + locator = new KeepLocator("7df44272090cee6c0732382bba415ee9+70+Ae8f48913fed782cbe463e0499ab37697ee06a2f8@5826180f"); + + // when + String actual = locator.toString(); + + // then + assertThat(actual).isEqualTo("7df44272090cee6c0732382bba415ee9+70+Ae8f48913fed782cbe463e0499ab37697ee06a2f8@5826180f"); + } +} diff --git a/src/test/java/org/arvados/client/test/utils/ApiClientTestUtils.java b/src/test/java/org/arvados/client/test/utils/ApiClientTestUtils.java new file mode 100644 index 0000000000..ac7dd02795 --- /dev/null +++ b/src/test/java/org/arvados/client/test/utils/ApiClientTestUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.test.utils; + +import org.arvados.client.config.FileConfigProvider; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; + +import static org.assertj.core.api.Assertions.assertThat; + +public final class ApiClientTestUtils { + + static final String BASE_URL = "/arvados/v1/"; + + private ApiClientTestUtils() {} + + public static MockResponse getResponse(String filename) throws IOException { + String filePath = String.format("src/test/resources/org/arvados/client/api/client/%s.json", filename); + File jsonFile = new File(filePath); + String json = FileUtils.readFileToString(jsonFile, Charset.defaultCharset()); + return new MockResponse().setBody(json); + } + + public static void assertAuthorizationHeader(RecordedRequest request) { + assertThat(request.getHeader("authorization")).isEqualTo("OAuth2 " + new FileConfigProvider().getApiToken()); + } + + public static void assertRequestPath(RecordedRequest request, String subPath) { + assertThat(request.getPath()).isEqualTo(BASE_URL + subPath); + } + + public static void assertRequestMethod(RecordedRequest request, RequestMethod requestMethod) { + assertThat(request.getMethod()).isEqualTo(requestMethod.name()); + } +} diff --git a/src/test/java/org/arvados/client/test/utils/ArvadosClientIntegrationTest.java b/src/test/java/org/arvados/client/test/utils/ArvadosClientIntegrationTest.java new file mode 100644 index 0000000000..59bd446934 --- /dev/null +++ b/src/test/java/org/arvados/client/test/utils/ArvadosClientIntegrationTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.test.utils; + +import org.arvados.client.config.FileConfigProvider; +import org.arvados.client.facade.ArvadosFacade; +import org.junit.BeforeClass; + +import static org.junit.Assert.assertTrue; + +public class ArvadosClientIntegrationTest { + + protected static final FileConfigProvider CONFIG = new FileConfigProvider("integration-tests-application.conf"); + protected static final ArvadosFacade FACADE = new ArvadosFacade(CONFIG); + protected static final String PROJECT_UUID = CONFIG.getIntegrationTestProjectUuid(); + + @BeforeClass + public static void validateConfiguration(){ + String msg = " info must be provided in configuration"; + CONFIG.getConfig().entrySet() + .forEach(e -> assertTrue("Parameter " + e.getKey() + msg, !e.getValue().render().equals("\"\""))); + } +} diff --git a/src/test/java/org/arvados/client/test/utils/ArvadosClientMockedWebServerTest.java b/src/test/java/org/arvados/client/test/utils/ArvadosClientMockedWebServerTest.java new file mode 100644 index 0000000000..74324b60fc --- /dev/null +++ b/src/test/java/org/arvados/client/test/utils/ArvadosClientMockedWebServerTest.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.test.utils; + +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; +import org.junit.Before; + +public class ArvadosClientMockedWebServerTest extends ArvadosClientUnitTest { + private static final int PORT = CONFIG.getApiPort(); + protected MockWebServer server = new MockWebServer(); + + @Before + public void setUpServer() throws Exception { + server.start(PORT); + } + + @After + public void tearDownServer() throws Exception { + server.shutdown(); + } +} diff --git a/src/test/java/org/arvados/client/test/utils/ArvadosClientUnitTest.java b/src/test/java/org/arvados/client/test/utils/ArvadosClientUnitTest.java new file mode 100644 index 0000000000..67566b697b --- /dev/null +++ b/src/test/java/org/arvados/client/test/utils/ArvadosClientUnitTest.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.test.utils; + +import org.arvados.client.config.FileConfigProvider; +import org.junit.BeforeClass; + +import static org.junit.Assert.assertTrue; + +public class ArvadosClientUnitTest { + + protected static final FileConfigProvider CONFIG = new FileConfigProvider("application.conf"); + + @BeforeClass + public static void validateConfiguration(){ + String msg = " info must be provided in configuration"; + CONFIG.getConfig().entrySet().forEach(e -> assertTrue("Parameter " + e.getKey() + msg, !e.getValue().render().equals("\"\""))); + } +} diff --git a/src/test/java/org/arvados/client/test/utils/FileTestUtils.java b/src/test/java/org/arvados/client/test/utils/FileTestUtils.java new file mode 100644 index 0000000000..295345093b --- /dev/null +++ b/src/test/java/org/arvados/client/test/utils/FileTestUtils.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.test.utils; + +import org.apache.commons.io.FileUtils; +import org.assertj.core.util.Lists; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.List; + +public class FileTestUtils { + + public static final String FILE_SPLIT_TEST_DIR = "/tmp/file-split"; + public static final String FILE_DOWNLOAD_TEST_DIR = "/tmp/arvados-downloaded"; + public static final String TEST_FILE = FILE_SPLIT_TEST_DIR + "/test-file"; + public static long ONE_FOURTH_GB = FileUtils.ONE_GB / 4; + public static long ONE_EIGTH_GB = FileUtils.ONE_GB / 8; + public static long HALF_GB = FileUtils.ONE_GB / 2; + public static int FILE_SPLIT_SIZE = 64; + + public static void createDirectory(String path) throws Exception { + new File(path).mkdirs(); + } + + public static void cleanDirectory(String directory) throws Exception { + FileUtils.cleanDirectory(new File(directory)); + } + + public static File generateFile(String path, long length) throws IOException { + RandomAccessFile testFile = new RandomAccessFile(path, "rwd"); + testFile.setLength(length); + testFile.close(); + return new File(path); + } + + public static List generatePredefinedFiles() throws IOException { + return Lists.newArrayList( + generateFile(TEST_FILE + 1, FileUtils.ONE_KB), + generateFile(TEST_FILE + 2, FileUtils.ONE_KB * 20), + generateFile(TEST_FILE + " " + 3, FileUtils.ONE_MB) + ); + } +} diff --git a/src/test/java/org/arvados/client/test/utils/RequestMethod.java b/src/test/java/org/arvados/client/test/utils/RequestMethod.java new file mode 100644 index 0000000000..53249c9884 --- /dev/null +++ b/src/test/java/org/arvados/client/test/utils/RequestMethod.java @@ -0,0 +1,13 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.test.utils; + +public enum RequestMethod { + + GET, POST, PUT, DELETE +} diff --git a/src/test/java/org/arvados/client/utils/FileMergeTest.java b/src/test/java/org/arvados/client/utils/FileMergeTest.java new file mode 100644 index 0000000000..00ca0b21bb --- /dev/null +++ b/src/test/java/org/arvados/client/utils/FileMergeTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.utils; + +import org.arvados.client.test.utils.FileTestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.List; + +import static org.arvados.client.test.utils.FileTestUtils.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class FileMergeTest { + + @Before + public void setUp() throws Exception { + FileTestUtils.createDirectory(FILE_SPLIT_TEST_DIR); + } + + @Test + public void fileChunksAreMergedIntoOneFile() throws Exception { + + // given + FileTestUtils.generateFile(TEST_FILE, FileTestUtils.ONE_EIGTH_GB); + + List files = FileSplit.split(new File(TEST_FILE), new File(FILE_SPLIT_TEST_DIR), FILE_SPLIT_SIZE); + File targetFile = new File(TEST_FILE); + + // when + FileMerge.merge(files, targetFile); + + // then + assertThat(targetFile.length()).isEqualTo(FileTestUtils.ONE_EIGTH_GB); + } + + @After + public void tearDown() throws Exception { + FileTestUtils.cleanDirectory(FILE_SPLIT_TEST_DIR); + } +} diff --git a/src/test/java/org/arvados/client/utils/FileSplitTest.java b/src/test/java/org/arvados/client/utils/FileSplitTest.java new file mode 100644 index 0000000000..4cc523ce4e --- /dev/null +++ b/src/test/java/org/arvados/client/utils/FileSplitTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) The Arvados Authors. All rights reserved. + * + * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0 + * + */ + +package org.arvados.client.utils; + +import org.arvados.client.test.utils.FileTestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.List; + +import static org.arvados.client.test.utils.FileTestUtils.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class FileSplitTest { + + @Before + public void setUp() throws Exception { + FileTestUtils.createDirectory(FILE_SPLIT_TEST_DIR); + } + + @Test + public void fileIsDividedIntoSmallerChunks() throws Exception { + + // given + int expectedSize = 2; + int expectedFileSizeInBytes = 67108864; + FileTestUtils.generateFile(TEST_FILE, FileTestUtils.ONE_EIGTH_GB); + + // when + List actual = FileSplit.split(new File(TEST_FILE), new File(FILE_SPLIT_TEST_DIR), FILE_SPLIT_SIZE); + + // then + assertThat(actual).hasSize(expectedSize); + assertThat(actual).allMatch(a -> a.length() == expectedFileSizeInBytes); + } + + @After + public void tearDown() throws Exception { + FileTestUtils.cleanDirectory(FILE_SPLIT_TEST_DIR); + } +} diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf new file mode 100644 index 0000000000..f19f3dc9a8 --- /dev/null +++ b/src/test/resources/application.conf @@ -0,0 +1,10 @@ +# configuration for unit tests + +arvados { + api { + port = 9000 + keepweb-port = 9000 + token = 1m69yw9m2wanubzyfkb1e9icplqhtr2r969bu9rnzqbqhb7cnb + protocol = "http" + } +} \ No newline at end of file diff --git a/src/test/resources/integration-tests-application.conf b/src/test/resources/integration-tests-application.conf new file mode 100644 index 0000000000..2f934d4cdf --- /dev/null +++ b/src/test/resources/integration-tests-application.conf @@ -0,0 +1,23 @@ +# Configuration for integration tests +# +# Remarks: +# * For example see integration-tests-application.conf.example +# * While providing data remove apostrophes ("") from each line +# * See Arvados documentation for information how to obtain a token: +# https://doc.arvados.org/user/reference/api-tokens.html +# + +arvados { + api { + keepweb-host = "" + keepweb-port = 443 + host = "" + port = 443 + token = "" + protocol = https + host-insecure = false + } + integration-tests { + project-uuid = "" + } +} diff --git a/src/test/resources/integration-tests-application.conf.example b/src/test/resources/integration-tests-application.conf.example new file mode 100644 index 0000000000..e57991806a --- /dev/null +++ b/src/test/resources/integration-tests-application.conf.example @@ -0,0 +1,16 @@ +# example configuration for integration tests + +arvados { + api { + keepweb-host = collections.ardev.mycompany.com + keepweb-port = 443 + host = api.ardev.mycompany.com + port = 443 + token = mytoken + protocol = https + host-insecure = false + } + integration-tests { + project-uuid = ardev-j7d0g-aa123f81q6y7skk + } +} \ No newline at end of file diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..ca6ee9cea8 --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/collections-create-manifest.json b/src/test/resources/org/arvados/client/api/client/collections-create-manifest.json new file mode 100644 index 0000000000..68dce30206 --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/collections-create-manifest.json @@ -0,0 +1,22 @@ +{ + "href": "/collections/112ci-4zz18-12tncxzptzbec1p", + "kind": "arvados#collection", + "etag": "bqoujj7oybdx0jybwvtsebj7y", + "uuid": "112ci-4zz18-12tncxzptzbec1p", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-21T13:38:56.521853000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-21T13:38:56.521853000Z", + "name": "Super Collection", + "description": null, + "properties": {}, + "portable_data_hash": "d41d8cd98f00b204e9800998ecf8427e+0", + "manifest_text": ". 7df44272090cee6c0732382bba415ee9+70+Aa5ece4560e3329315165b36c239b8ab79c888f8a@5a1d5708 0:70:README.md\n", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/collections-create-simple.json b/src/test/resources/org/arvados/client/api/client/collections-create-simple.json new file mode 100644 index 0000000000..57a2ee5a5b --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/collections-create-simple.json @@ -0,0 +1,22 @@ +{ + "href": "/collections/112ci-4zz18-12tncxzptzbec1p", + "kind": "arvados#collection", + "etag": "bqoujj7oybdx0jybwvtsebj7y", + "uuid": "112ci-4zz18-12tncxzptzbec1p", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-21T13:38:56.521853000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-21T13:38:56.521853000Z", + "name": "Super Collection", + "description": null, + "properties": {}, + "portable_data_hash": "d41d8cd98f00b204e9800998ecf8427e+0", + "manifest_text": "", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/collections-download-file.json b/src/test/resources/org/arvados/client/api/client/collections-download-file.json new file mode 100644 index 0000000000..1fed3832b0 --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/collections-download-file.json @@ -0,0 +1,22 @@ +{ + "href": "/collections/ardev-4zz18-jk5vo4uo9u5vj52", + "kind": "arvados#collection", + "etag": "2vm76dxmzr23u9774iguuxsrg", + "uuid": "ardev-4zz18-jk5vo4uo9u5vj52", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-02-19T11:00:00.852389000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-02-19T11:00:00.852389000Z", + "name": "New Collection (2018-02-19 12:00:00.273)", + "description": null, + "properties": {}, + "portable_data_hash": "49581091dfad651945c12b08d4735d88+112", + "manifest_text": ". 163679d58edaadc28db769011728a72c+1070080+A3acf8c1fe582c265d2077702e4a7d74fcc03aba8@5aa4fdeb 0:1024:test-file1 1024:20480:test-file2 21504:1048576:test-file\\0403\n", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/collections-get.json b/src/test/resources/org/arvados/client/api/client/collections-get.json new file mode 100644 index 0000000000..e8fdd83e71 --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/collections-get.json @@ -0,0 +1,22 @@ +{ + "href": "/collections/112ci-4zz18-p51w7z3fpopo6sm", + "kind": "arvados#collection", + "etag": "52tk5yg024cwhkkcidu3zcmj2", + "uuid": "112ci-4zz18-p51w7z3fpopo6sm", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-15T10:36:03.554356000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-15T10:36:03.554356000Z", + "name": "Collection With Manifest #2", + "description": null, + "properties": {}, + "portable_data_hash": "6c4106229b08fe25f48b3a7a8289dd46+143", + "manifest_text": ". 66c9daa69630e092e9ce554b7aae8a20+524288+A4a15ffea58f259e09f68d3f7eea29942750a79d0@5a269ff6 435f38dd384b06c248feabee0cabca52+524288+A8a99e8148bd368c49901526098901bb7d7890c3b@5a269ff6 dc5b6c104aab35fff6d70a4dadc28d37+391727+Ab0662d549c422c983fccaad02b4ade7b48a8255b@5a269ff6 0:1440303:lombok.jar\n", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/collections-list.json b/src/test/resources/org/arvados/client/api/client/collections-list.json new file mode 100644 index 0000000000..86a3bdafbb --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/collections-list.json @@ -0,0 +1,871 @@ +{ + "kind": "arvados#collectionList", + "etag": "", + "self_link": "", + "offset": 0, + "limit": 100, + "items": [ + { + "href": "/collections/112ci-4zz18-x6xfmvz0chnkzgv", + "kind": "arvados#collection", + "etag": "8xyiwnih5b5vzmj5sa33348a7", + "uuid": "112ci-4zz18-x6xfmvz0chnkzgv", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-15T13:06:36.934337000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-15T13:06:36.934337000Z", + "name": "Collection With Manifest #3", + "description": null, + "properties": {}, + "portable_data_hash": "6c4106229b08fe25f48b3a7a8289dd46+143", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-p51w7z3fpopo6sm", + "kind": "arvados#collection", + "etag": "8cmhep8aixe4p42pxjoct5502", + "uuid": "112ci-4zz18-p51w7z3fpopo6sm", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-15T10:36:03.554356000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-15T10:36:03.554356000Z", + "name": "Collection With Manifest #2", + "description": null, + "properties": {}, + "portable_data_hash": "6c4106229b08fe25f48b3a7a8289dd46+143", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-xb6gf2yraln7cwa", + "kind": "arvados#collection", + "etag": "de2ol2dyvsba3mn46al760cyg", + "uuid": "112ci-4zz18-xb6gf2yraln7cwa", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-15T09:32:44.146172000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-15T09:32:44.146172000Z", + "name": "New collection", + "description": null, + "properties": {}, + "portable_data_hash": "d41d8cd98f00b204e9800998ecf8427e+0", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-r5jfktpn3a9o0ap", + "kind": "arvados#collection", + "etag": "dby68gd0vatvi090cu0axvtq3", + "uuid": "112ci-4zz18-r5jfktpn3a9o0ap", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-14T13:00:35.431046000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-14T13:00:35.431046000Z", + "name": "Collection With Manifest #1", + "description": null, + "properties": {}, + "portable_data_hash": "3c59518bf8e1100d420488d822682b4a+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-nqxk8xjn6mtskzt", + "kind": "arvados#collection", + "etag": "2b34uzau862w862a2rv36agv6", + "uuid": "112ci-4zz18-nqxk8xjn6mtskzt", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-14T12:59:34.767068000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-14T12:59:34.767068000Z", + "name": "Empty Collection #2", + "description": null, + "properties": {}, + "portable_data_hash": "d41d8cd98f00b204e9800998ecf8427e+0", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-rs9bcf5qnyfjrkm", + "kind": "arvados#collection", + "etag": "60aywazztwfspnasltufcjxpa", + "uuid": "112ci-4zz18-rs9bcf5qnyfjrkm", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-14T12:52:33.124452000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-14T12:52:33.124452000Z", + "name": "Empty Collection #1", + "description": null, + "properties": {}, + "portable_data_hash": "d41d8cd98f00b204e9800998ecf8427e+0", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-af656lee4kv7q2m", + "kind": "arvados#collection", + "etag": "1jward6snif3tsjzftxh8hvwh", + "uuid": "112ci-4zz18-af656lee4kv7q2m", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-14T12:09:05.319319000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-14T12:09:05.319319000Z", + "name": "create example", + "description": null, + "properties": {}, + "portable_data_hash": "d41d8cd98f00b204e9800998ecf8427e+0", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-y2zqix7k9an7nro", + "kind": "arvados#collection", + "etag": "zs2n4zliu6nb5yk3rw6h5ugw", + "uuid": "112ci-4zz18-y2zqix7k9an7nro", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-13T16:59:02.299257000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-13T16:59:02.299257000Z", + "name": "Saved at 2017-11-13 16:59:01 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-wq77jfi62u5i4rv", + "kind": "arvados#collection", + "etag": "eijhemzgy44ofmu0dtrowl604", + "uuid": "112ci-4zz18-wq77jfi62u5i4rv", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-13T16:58:10.637548000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-13T16:58:10.637548000Z", + "name": "Saved at 2017-11-13 16:58:07 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-unaeckkjgeg7ui0", + "kind": "arvados#collection", + "etag": "1oq7ye0gfbf3ih6y864w3n683", + "uuid": "112ci-4zz18-unaeckkjgeg7ui0", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-10T09:43:07.583862000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-10T09:43:07.583862000Z", + "name": "Saved at 2017-11-10 09:43:03 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-5y6atonkxq55lms", + "kind": "arvados#collection", + "etag": "4qmqlro878yx8q7ikhilo8qwn", + "uuid": "112ci-4zz18-5y6atonkxq55lms", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T12:46:15.245770000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T12:46:15.245770000Z", + "name": "Saved at 2017-11-09 12:46:13 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-b3fjqd01pxjvseo", + "kind": "arvados#collection", + "etag": "91v698hngoz241c38bbmh0ogc", + "uuid": "112ci-4zz18-b3fjqd01pxjvseo", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:54:07.259998000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:54:07.259998000Z", + "name": "Saved at 2017-11-09 11:54:04 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-cwfxl8h41q18n65", + "kind": "arvados#collection", + "etag": "215t842ckrrgjpxrxr4j0gsui", + "uuid": "112ci-4zz18-cwfxl8h41q18n65", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:49:38.276888000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:49:38.276888000Z", + "name": "Saved at 2017-11-09 11:49:35 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-uv4xu08739tn1vy", + "kind": "arvados#collection", + "etag": "90z6i3oqv197osng3wvjjir3t", + "uuid": "112ci-4zz18-uv4xu08739tn1vy", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:43:05.917513000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:43:05.917513000Z", + "name": "Saved at 2017-11-09 11:43:05 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-pzisn8c5mefzczv", + "kind": "arvados#collection", + "etag": "5lcf6wvc3wypwobswdz22wen", + "uuid": "112ci-4zz18-pzisn8c5mefzczv", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:40:38.804718000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:40:38.804718000Z", + "name": "Saved at 2017-11-09 11:40:36 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-mj24uwtnqqrno27", + "kind": "arvados#collection", + "etag": "98s08xew49avui1gy3mzit8je", + "uuid": "112ci-4zz18-mj24uwtnqqrno27", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:40:25.189869000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:40:25.189869000Z", + "name": "Saved at 2017-11-09 11:40:24 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-oco162516upgqng", + "kind": "arvados#collection", + "etag": "a09wnvl4i51xqx7u9yf4qbi94", + "uuid": "112ci-4zz18-oco162516upgqng", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:39:04.148785000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:39:04.148785000Z", + "name": "Saved at 2017-11-09 11:39:03 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-tlze7dgczsdwkep", + "kind": "arvados#collection", + "etag": "4ee2xudbc5rkr597drgu9tg10", + "uuid": "112ci-4zz18-tlze7dgczsdwkep", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:37:59.478975000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:37:59.478975000Z", + "name": "Saved at 2017-11-09 11:37:58 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-nq0kxi9d7w64la1", + "kind": "arvados#collection", + "etag": "5aa3evnbceo3brnps2e1sq8ts", + "uuid": "112ci-4zz18-nq0kxi9d7w64la1", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:32:23.329259000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:32:23.329259000Z", + "name": "Saved at 2017-11-09 11:32:22 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-fks9mewtw155pvx", + "kind": "arvados#collection", + "etag": "97vicgogv8bovmk4s2jymsdq", + "uuid": "112ci-4zz18-fks9mewtw155pvx", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:30:17.589462000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:30:17.589462000Z", + "name": "Saved at 2017-11-09 11:30:17 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-kp356e0q2wdl2df", + "kind": "arvados#collection", + "etag": "btktwjclv063s1rd6duvk51v3", + "uuid": "112ci-4zz18-kp356e0q2wdl2df", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:29:26.820481000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:29:26.820481000Z", + "name": "Saved at 2017-11-09 11:29:25 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-0ey8ob38xf7surq", + "kind": "arvados#collection", + "etag": "bob83na42pufqli1a5buxryvm", + "uuid": "112ci-4zz18-0ey8ob38xf7surq", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:08:53.781498000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:08:53.781498000Z", + "name": "Saved at 2017-11-09 11:08:52 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-wu2n0fv3cewna1n", + "kind": "arvados#collection", + "etag": "7pl1x327eeutqtsjppdj284g8", + "uuid": "112ci-4zz18-wu2n0fv3cewna1n", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T11:08:33.423284000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T11:08:33.423284000Z", + "name": "Saved at 2017-11-09 11:08:33 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-hyybo6yuvkx4hrm", + "kind": "arvados#collection", + "etag": "2wg1wn2o18ubrgbhbqwwsslhf", + "uuid": "112ci-4zz18-hyybo6yuvkx4hrm", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T10:44:53.096798000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T10:44:53.096798000Z", + "name": "Saved at 2017-11-09 10:44:51 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-h3gjq7gzd4syanw", + "kind": "arvados#collection", + "etag": "8jk0at4e69cwjyjamvm4wz2oj", + "uuid": "112ci-4zz18-h3gjq7gzd4syanw", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T10:41:31.278281000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T10:41:31.278281000Z", + "name": "Saved at 2017-11-09 10:41:30 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-jinwyyaeigjs1yg", + "kind": "arvados#collection", + "etag": "be57zhzufz2hp1tbdwidoro5j", + "uuid": "112ci-4zz18-jinwyyaeigjs1yg", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T10:41:07.083017000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T10:41:07.083017000Z", + "name": "Saved at 2017-11-09 10:41:06 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-etf8aghyxlfxvo1", + "kind": "arvados#collection", + "etag": "29lj2roie4cygo5ffgrduflly", + "uuid": "112ci-4zz18-etf8aghyxlfxvo1", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T10:40:31.710865000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T10:40:31.710865000Z", + "name": "Saved at 2017-11-09 10:40:31 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-jtbn4edpkkhbm9b", + "kind": "arvados#collection", + "etag": "6div78e1nhusii4x1xkp3rg2v", + "uuid": "112ci-4zz18-jtbn4edpkkhbm9b", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T10:39:36.999602000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T10:39:36.999602000Z", + "name": "Saved at 2017-11-09 10:39:36 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-whdleimp34hiqp6", + "kind": "arvados#collection", + "etag": "12wlbsxlmy3sze4v2m0ua7ake", + "uuid": "112ci-4zz18-whdleimp34hiqp6", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T10:19:52.879907000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T10:19:52.879907000Z", + "name": "Saved at 2017-11-09 10:19:52 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-kj8dz72zpo5kbtm", + "kind": "arvados#collection", + "etag": "9bv1bw9afb3w84gu55uzcgd6h", + "uuid": "112ci-4zz18-kj8dz72zpo5kbtm", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T10:16:31.558621000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T10:16:31.558621000Z", + "name": "Saved at 2017-11-09 10:16:30 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "5ba3fc508718fabfa20d24390fe31856+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-tr306nau9hrr437", + "kind": "arvados#collection", + "etag": "683d77tvlhe97etk9bk2bx8ds", + "uuid": "112ci-4zz18-tr306nau9hrr437", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T09:59:44.978811000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T09:59:44.978811000Z", + "name": "Saved at 2017-11-09 09:59:44 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "67cbebb9f739b6b06ca056d21115cf43+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-oxuk69569mxztp0", + "kind": "arvados#collection", + "etag": "1m34v9jbna2v7gv7auio54i8w", + "uuid": "112ci-4zz18-oxuk69569mxztp0", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T09:59:30.774888000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T09:59:30.774888000Z", + "name": "Saved at 2017-11-09 09:59:30 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "67cbebb9f739b6b06ca056d21115cf43+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-wf8sl6xbyfwjyer", + "kind": "arvados#collection", + "etag": "7l2a9fhqmxg7ghn7osx0s19v4", + "uuid": "112ci-4zz18-wf8sl6xbyfwjyer", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T09:58:21.496088000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T09:58:21.496088000Z", + "name": "Saved at 2017-11-09 09:58:20 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "67cbebb9f739b6b06ca056d21115cf43+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-drpia2es1hp9ydi", + "kind": "arvados#collection", + "etag": "33dw426fhs2vlb50b6301ukn0", + "uuid": "112ci-4zz18-drpia2es1hp9ydi", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T09:56:08.506505000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T09:56:08.506505000Z", + "name": "Saved at 2017-11-09 09:56:08 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "67cbebb9f739b6b06ca056d21115cf43+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-5b4px2i2dwyidfi", + "kind": "arvados#collection", + "etag": "2437tnhn2gmti52lpm8nfq9ct", + "uuid": "112ci-4zz18-5b4px2i2dwyidfi", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T09:54:06.651026000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T09:54:06.651026000Z", + "name": "Saved at 2017-11-09 09:54:06 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "67cbebb9f739b6b06ca056d21115cf43+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-94oslnwnxe1f9wp", + "kind": "arvados#collection", + "etag": "7e0k48zu93o57zudxjp1yrgjq", + "uuid": "112ci-4zz18-94oslnwnxe1f9wp", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T09:40:04.240297000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T09:40:04.240297000Z", + "name": "Saved at 2017-11-09 09:39:58 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "67cbebb9f739b6b06ca056d21115cf43+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-2fk0d5d4jjc1fmq", + "kind": "arvados#collection", + "etag": "cuirr803f54e89reakuq50oaq", + "uuid": "112ci-4zz18-2fk0d5d4jjc1fmq", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T09:36:14.952671000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T09:36:14.952671000Z", + "name": "Saved at 2017-11-09 09:36:08 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "67cbebb9f739b6b06ca056d21115cf43+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-xp9pu81xyc5h422", + "kind": "arvados#collection", + "etag": "3bi5xd8ezxrazk5266cwzn4s4", + "uuid": "112ci-4zz18-xp9pu81xyc5h422", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T09:35:29.552746000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T09:35:29.552746000Z", + "name": "Saved at 2017-11-09 09:35:29 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "67cbebb9f739b6b06ca056d21115cf43+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-znb4lo0if2as58c", + "kind": "arvados#collection", + "etag": "59uaoxy6uh82i6lrvr3ht8gz1", + "uuid": "112ci-4zz18-znb4lo0if2as58c", + "owner_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "created_at": "2017-11-09T09:31:08.109971000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-09T09:31:08.109971000Z", + "name": "Saved at 2017-11-09 09:31:06 UTC by VirtualBox", + "description": null, + "properties": {}, + "portable_data_hash": "67cbebb9f739b6b06ca056d21115cf43+53", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-6pvl5ea5u932qzi", + "kind": "arvados#collection", + "etag": "dksrh8jznxoaidl29i1vv5904", + "uuid": "112ci-4zz18-6pvl5ea5u932qzi", + "owner_uuid": "112ci-j7d0g-tw71k7mxii6fqgx", + "created_at": "2017-11-08T12:48:32.238698000Z", + "modified_by_client_uuid": "112ci-ozdt8-f4633qdjs6w8zcy", + "modified_by_user_uuid": "112ci-tpzed-nd84czdo4iea1mz", + "modified_at": "2017-11-08T12:50:23.946608000Z", + "name": "New collection", + "description": null, + "properties": {}, + "portable_data_hash": "18c037c51c3f74be53ea2b115afd0c5f+69", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/collections/112ci-4zz18-wq5pyrxfv1t9isu", + "kind": "arvados#collection", + "etag": "1w1rhhd6oql4ceb7h9t16sf0q", + "uuid": "112ci-4zz18-wq5pyrxfv1t9isu", + "owner_uuid": "112ci-j7d0g-anonymouspublic", + "created_at": "2017-11-03T10:03:20.364737000Z", + "modified_by_client_uuid": null, + "modified_by_user_uuid": "112ci-tpzed-000000000000000", + "modified_at": "2017-11-03T10:03:20.364737000Z", + "name": null, + "description": null, + "properties": {}, + "portable_data_hash": "d41d8cd98f00b204e9800998ecf8427e+0", + "replication_desired": null, + "replication_confirmed": null, + "replication_confirmed_at": null, + "delete_at": null, + "trash_at": null, + "is_trashed": false + } + ], + "items_available": 41 +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/groups-get.json b/src/test/resources/org/arvados/client/api/client/groups-get.json new file mode 100644 index 0000000000..f1834e749c --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/groups-get.json @@ -0,0 +1,21 @@ +{ + "href": "/groups/ardev-j7d0g-bmg3pfqtx3ivczp", + "kind": "arvados#group", + "etag": "3hw0vk4mbl0ofvia5k6x4dwrx", + "uuid": "ardev-j7d0g-bmg3pfqtx3ivczp", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-03-29T11:09:05.984597000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-03-29T11:09:05.984597000Z", + "name": "TestGroup1", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/groups-list.json b/src/test/resources/org/arvados/client/api/client/groups-list.json new file mode 100644 index 0000000000..fa74e1cb53 --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/groups-list.json @@ -0,0 +1,430 @@ +{ + "kind": "arvados#groupList", + "etag": "", + "self_link": "", + "offset": 0, + "limit": 100, + "items": [ + { + "href": "/groups/ardev-j7d0g-ylx7wnu1moge2di", + "kind": "arvados#group", + "etag": "68vubv3iw7663763bozxebmyf", + "uuid": "ardev-j7d0g-ylx7wnu1moge2di", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-18T09:09:21.126649000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-18T09:09:21.126649000Z", + "name": "TestProject1", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-mnzhga726itrbrq", + "kind": "arvados#group", + "etag": "68q7r8r37u9hckr2zsynvton3", + "uuid": "ardev-j7d0g-mnzhga726itrbrq", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-17T12:11:24.389594000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-17T12:11:24.389594000Z", + "name": "TestProject2", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-0w9m1sz46ljtdnm", + "kind": "arvados#group", + "etag": "ef4vzx5gyudkrg9zml0zdv6qu", + "uuid": "ardev-j7d0g-0w9m1sz46ljtdnm", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-17T12:08:39.066802000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-17T12:08:39.066802000Z", + "name": "TestProject3", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-r20iem5ou6h5wao", + "kind": "arvados#group", + "etag": "6h6h4ta6yyf9058delxk8fnqs", + "uuid": "ardev-j7d0g-r20iem5ou6h5wao", + "owner_uuid": "ardev-j7d0g-j7drd8yikkp6evd", + "created_at": "2018-04-17T12:03:39.647244000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-17T12:03:39.647244000Z", + "name": "TestProject4", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-j7d0g-j7drd8yikkp6evd", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-j7drd8yikkp6evd", + "kind": "arvados#group", + "etag": "6se2y8f9o7uu06pbopgq56xds", + "uuid": "ardev-j7d0g-j7drd8yikkp6evd", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-17T11:58:31.339515000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-17T11:58:31.339515000Z", + "name": "TestProject5", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-kh1g7i5va870xt0", + "kind": "arvados#group", + "etag": "2si26vaig3vig9266pqkqh2gy", + "uuid": "ardev-j7d0g-kh1g7i5va870xt0", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-17T10:56:54.391676000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-17T10:56:54.391676000Z", + "name": "TestProject6", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-sclkdyuwm4h2m78", + "kind": "arvados#group", + "etag": "edgnz6q0vt2u3o13ujtfohb75", + "uuid": "ardev-j7d0g-sclkdyuwm4h2m78", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-17T10:27:15.914517000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-17T10:27:15.914517000Z", + "name": "TestProject7", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-593khc577zuyyhe", + "kind": "arvados#group", + "etag": "39ig9ttgec6lbe096uetn2cb9", + "uuid": "ardev-j7d0g-593khc577zuyyhe", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-17T10:27:03.858203000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-17T10:27:03.858203000Z", + "name": "TestProject8", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-iotds0tm559dbz7", + "kind": "arvados#group", + "etag": "1dpr8v6tx6pta0fozq93eyeou", + "uuid": "ardev-j7d0g-iotds0tm559dbz7", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-17T10:26:25.180623000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-17T10:26:25.180623000Z", + "name": "TestProject9", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-gbqay74778tonb8", + "kind": "arvados#group", + "etag": "dizbavs2opfe1wpx6thocfki0", + "uuid": "ardev-j7d0g-gbqay74778tonb8", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-17T10:26:06.435961000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-17T10:26:06.435961000Z", + "name": "TestProject10", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-fmq1t0jlznehbdm", + "kind": "arvados#group", + "etag": "6xue8m3lx9qpptfvdf13val5t", + "uuid": "ardev-j7d0g-fmq1t0jlznehbdm", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-17T10:25:55.546399000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-17T10:25:55.546399000Z", + "name": "TestProject11", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-vxju56ch64u51gq", + "kind": "arvados#group", + "etag": "2gqix9e4m023usi9exhrsjx6z", + "uuid": "ardev-j7d0g-vxju56ch64u51gq", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-16T14:09:49.700566000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-16T14:09:49.700566000Z", + "name": "TestProject12", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-g8m4w0d22gv6fbj", + "kind": "arvados#group", + "etag": "73n8x82814o6ihld0kltf468d", + "uuid": "ardev-j7d0g-g8m4w0d22gv6fbj", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-11T15:02:35.016850000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-11T15:02:35.016850000Z", + "name": "TestProject13", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-lstqed4y78khaqm", + "kind": "arvados#group", + "etag": "91f7uwq7pj3d3ez1u4smjg3ch", + "uuid": "ardev-j7d0g-lstqed4y78khaqm", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-06T15:29:27.754408000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-06T15:29:27.754408000Z", + "name": "TestProject14", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-0jbezvnq8i07l7p", + "kind": "arvados#group", + "etag": "7dbxhvbcfaogwnvo8k4mtqthk", + "uuid": "ardev-j7d0g-0jbezvnq8i07l7p", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-04-05T09:32:46.946417000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-04-05T09:32:46.946417000Z", + "name": "TestProject15", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-72dxer22g6iltqz", + "kind": "arvados#group", + "etag": "dhfu203rckzdzvx832wm7jv59", + "uuid": "ardev-j7d0g-72dxer22g6iltqz", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-03-29T11:27:02.482218000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-03-29T13:17:00.045606000Z", + "name": "TestProject16", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-nebzwquxtq1v3o5", + "kind": "arvados#group", + "etag": "7l9oxbdf4e1m9ddnujokf7czz", + "uuid": "ardev-j7d0g-nebzwquxtq1v3o5", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-03-29T11:11:26.235411000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-03-29T11:11:26.235411000Z", + "name": "TestProject17", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-5589c8dmxevecqh", + "kind": "arvados#group", + "etag": "83862x2o4453mja2rvypjl5gv", + "uuid": "ardev-j7d0g-5589c8dmxevecqh", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-03-29T11:10:58.496482000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-03-29T11:10:58.496482000Z", + "name": "TestProject18", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-bmg3pfqtx3ivczp", + "kind": "arvados#group", + "etag": "3hw0vk4mbl0ofvia5k6x4dwrx", + "uuid": "ardev-j7d0g-bmg3pfqtx3ivczp", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-03-29T11:09:05.984597000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-03-29T11:09:05.984597000Z", + "name": "TestProject19", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + }, + { + "href": "/groups/ardev-j7d0g-mfitz2oa4rpycou", + "kind": "arvados#group", + "etag": "6p9xbxpttj782mpqs537gfvc6", + "uuid": "ardev-j7d0g-mfitz2oa4rpycou", + "owner_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "created_at": "2018-03-29T11:00:19.809612000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4", + "modified_at": "2018-03-29T11:00:19.809612000Z", + "name": "TestProject20", + "group_class": "project", + "description": null, + "writable_by": [ + "ardev-tpzed-n3kzq4fvoks3uw4", + "ardev-tpzed-n3kzq4fvoks3uw4" + ], + "delete_at": null, + "trash_at": null, + "is_trashed": false + } + ], + "items_available": 20 +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/keep-client-test-file.txt b/src/test/resources/org/arvados/client/api/client/keep-client-test-file.txt new file mode 100644 index 0000000000..5cbed85e5e --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/keep-client-test-file.txt @@ -0,0 +1 @@ +Sample text file to test keep client. \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/keep-services-accessible-disk-only.json b/src/test/resources/org/arvados/client/api/client/keep-services-accessible-disk-only.json new file mode 100644 index 0000000000..d5bd0d83d1 --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/keep-services-accessible-disk-only.json @@ -0,0 +1,42 @@ +{ + "kind": "arvados#keepServiceList", + "etag": "", + "self_link": "", + "offset": null, + "limit": null, + "items": [ + { + "href": "/keep_services/112ci-bi6l4-hv02fg8sbti8ykk", + "kind": "arvados#keepService", + "etag": "bjzh7og2d9z949lbd38vnnslt", + "uuid": "112ci-bi6l4-hv02fg8sbti8ykk", + "owner_uuid": "112ci-tpzed-000000000000000", + "created_at": "2017-11-03T10:04:48.314229000Z", + "modified_by_client_uuid": "112ci-ozdt8-xxy0ipzwti8gnmt", + "modified_by_user_uuid": "112ci-tpzed-000000000000000", + "modified_at": "2017-11-03T10:04:48.314229000Z", + "service_host": "localhost", + "service_port": 9000, + "service_ssl_flag": false, + "service_type": "disk", + "read_only": false + }, + { + "href": "/keep_services/112ci-bi6l4-f0r03wrqymotwql", + "kind": "arvados#keepService", + "etag": "7m64l69kko4bytpsykf8cay7t", + "uuid": "112ci-bi6l4-f0r03wrqymotwql", + "owner_uuid": "112ci-tpzed-000000000000000", + "created_at": "2017-11-03T10:04:48.351577000Z", + "modified_by_client_uuid": "112ci-ozdt8-xxy0ipzwti8gnmt", + "modified_by_user_uuid": "112ci-tpzed-000000000000000", + "modified_at": "2017-11-03T10:04:48.351577000Z", + "service_host": "localhost", + "service_port": 9001, + "service_ssl_flag": false, + "service_type": "disk", + "read_only": false + } + ], + "items_available": 2 +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/keep-services-accessible.json b/src/test/resources/org/arvados/client/api/client/keep-services-accessible.json new file mode 100644 index 0000000000..3d95cf932f --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/keep-services-accessible.json @@ -0,0 +1,42 @@ +{ + "kind": "arvados#keepServiceList", + "etag": "", + "self_link": "", + "offset": null, + "limit": null, + "items": [ + { + "href": "/keep_services/112ci-bi6l4-hv02fg8sbti8ykk", + "kind": "arvados#keepService", + "etag": "bjzh7og2d9z949lbd38vnnslt", + "uuid": "112ci-bi6l4-hv02fg8sbti8ykk", + "owner_uuid": "112ci-tpzed-000000000000000", + "created_at": "2017-11-03T10:04:48.314229000Z", + "modified_by_client_uuid": "112ci-ozdt8-xxy0ipzwti8gnmt", + "modified_by_user_uuid": "112ci-tpzed-000000000000000", + "modified_at": "2017-11-03T10:04:48.314229000Z", + "service_host": "localhost", + "service_port": 9000, + "service_ssl_flag": false, + "service_type": "disk", + "read_only": false + }, + { + "href": "/keep_services/112ci-bi6l4-f0r03wrqymotwql", + "kind": "arvados#keepService", + "etag": "7m64l69kko4bytpsykf8cay7t", + "uuid": "112ci-bi6l4-f0r03wrqymotwql", + "owner_uuid": "112ci-tpzed-000000000000000", + "created_at": "2017-11-03T10:04:48.351577000Z", + "modified_by_client_uuid": "112ci-ozdt8-xxy0ipzwti8gnmt", + "modified_by_user_uuid": "112ci-tpzed-000000000000000", + "modified_at": "2017-11-03T10:04:48.351577000Z", + "service_host": "localhost", + "service_port": 9000, + "service_ssl_flag": false, + "service_type": "gpfs", + "read_only": false + } + ], + "items_available": 2 +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/keep-services-get.json b/src/test/resources/org/arvados/client/api/client/keep-services-get.json new file mode 100644 index 0000000000..f3c289497c --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/keep-services-get.json @@ -0,0 +1,16 @@ +{ + "href": "/keep_services/112ci-bi6l4-hv02fg8sbti8ykk", + "kind": "arvados#keepService", + "etag": "bjzh7og2d9z949lbd38vnnslt", + "uuid": "112ci-bi6l4-hv02fg8sbti8ykk", + "owner_uuid": "112ci-tpzed-000000000000000", + "created_at": "2017-11-03T10:04:48.314229000Z", + "modified_by_client_uuid": "112ci-ozdt8-xxy0ipzwti8gnmt", + "modified_by_user_uuid": "112ci-tpzed-000000000000000", + "modified_at": "2017-11-03T10:04:48.314229000Z", + "service_host": "10.0.2.15", + "service_port": 9000, + "service_ssl_flag": false, + "service_type": "disk", + "read_only": false +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/keep-services-list.json b/src/test/resources/org/arvados/client/api/client/keep-services-list.json new file mode 100644 index 0000000000..90ba91631e --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/keep-services-list.json @@ -0,0 +1,58 @@ +{ + "kind": "arvados#keepServiceList", + "etag": "", + "self_link": "", + "offset": 0, + "limit": 100, + "items": [ + { + "href": "/keep_services/112ci-bi6l4-f0r03wrqymotwql", + "kind": "arvados#keepService", + "etag": "7m64l69kko4bytpsykf8cay7t", + "uuid": "112ci-bi6l4-f0r03wrqymotwql", + "owner_uuid": "112ci-tpzed-000000000000000", + "created_at": "2017-11-03T10:04:48.351577000Z", + "modified_by_client_uuid": "112ci-ozdt8-xxy0ipzwti8gnmt", + "modified_by_user_uuid": "112ci-tpzed-000000000000000", + "modified_at": "2017-11-03T10:04:48.351577000Z", + "service_host": "10.0.2.15", + "service_port": 9000, + "service_ssl_flag": false, + "service_type": "disk", + "read_only": false + }, + { + "href": "/keep_services/112ci-bi6l4-hv02fg8sbti8ykk", + "kind": "arvados#keepService", + "etag": "bjzh7og2d9z949lbd38vnnslt", + "uuid": "112ci-bi6l4-hv02fg8sbti8ykk", + "owner_uuid": "112ci-tpzed-000000000000000", + "created_at": "2017-11-03T10:04:48.314229000Z", + "modified_by_client_uuid": "112ci-ozdt8-xxy0ipzwti8gnmt", + "modified_by_user_uuid": "112ci-tpzed-000000000000000", + "modified_at": "2017-11-03T10:04:48.314229000Z", + "service_host": "10.0.2.15", + "service_port": 9001, + "service_ssl_flag": false, + "service_type": "disk", + "read_only": false + }, + { + "href": "/keep_services/112ci-bi6l4-ko27cfbsf2ssx2m", + "kind": "arvados#keepService", + "etag": "4be61qkpt6nzdfff4vj9nkpmj", + "uuid": "112ci-bi6l4-ko27cfbsf2ssx2m", + "owner_uuid": "112ci-tpzed-000000000000000", + "created_at": "2017-11-03T10:04:36.355045000Z", + "modified_by_client_uuid": "112ci-ozdt8-xxy0ipzwti8gnmt", + "modified_by_user_uuid": "112ci-tpzed-000000000000000", + "modified_at": "2017-11-03T10:04:36.355045000Z", + "service_host": "10.0.2.15", + "service_port": 9002, + "service_ssl_flag": false, + "service_type": "proxy", + "read_only": false + } + ], + "items_available": 3 +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/keep-services-not-accessible.json b/src/test/resources/org/arvados/client/api/client/keep-services-not-accessible.json new file mode 100644 index 0000000000..c930ee2ce1 --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/keep-services-not-accessible.json @@ -0,0 +1,9 @@ +{ + "kind": "arvados#keepServiceList", + "etag": "", + "self_link": "", + "offset": null, + "limit": null, + "items": [], + "items_available": 0 +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/users-create.json b/src/test/resources/org/arvados/client/api/client/users-create.json new file mode 100644 index 0000000000..87d09ab961 --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/users-create.json @@ -0,0 +1,26 @@ +{ + "href": "/users/ardev-tpzed-q6dvn7sby55up1b", + "kind": "arvados#user", + "etag": "b21emst9eu9u1wdpqcz6la583", + "uuid": "ardev-tpzed-q6dvn7sby55up1b", + "owner_uuid": "ardev-tpzed-000000000000000", + "created_at": "2017-10-30T19:42:43.324740000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-o3km4ug9jhs189j", + "modified_at": "2017-10-31T09:01:03.985749000Z", + "email": "example@email.com", + "username": "johnwayne", + "full_name": "John Wayne", + "first_name": "John", + "last_name": "Wayne", + "identity_url": "ardev-tpzed-r09t5ztf5qd3rlj", + "is_active": true, + "is_admin": null, + "is_invited": true, + "prefs": {}, + "writable_by": [ + "ardev-tpzed-000000000000000", + "ardev-tpzed-q6dvn7sby55up1b", + "ardev-j7d0g-000000000000000" + ] +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/users-get.json b/src/test/resources/org/arvados/client/api/client/users-get.json new file mode 100644 index 0000000000..87d09ab961 --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/users-get.json @@ -0,0 +1,26 @@ +{ + "href": "/users/ardev-tpzed-q6dvn7sby55up1b", + "kind": "arvados#user", + "etag": "b21emst9eu9u1wdpqcz6la583", + "uuid": "ardev-tpzed-q6dvn7sby55up1b", + "owner_uuid": "ardev-tpzed-000000000000000", + "created_at": "2017-10-30T19:42:43.324740000Z", + "modified_by_client_uuid": "ardev-ozdt8-97tzh5x96spqkay", + "modified_by_user_uuid": "ardev-tpzed-o3km4ug9jhs189j", + "modified_at": "2017-10-31T09:01:03.985749000Z", + "email": "example@email.com", + "username": "johnwayne", + "full_name": "John Wayne", + "first_name": "John", + "last_name": "Wayne", + "identity_url": "ardev-tpzed-r09t5ztf5qd3rlj", + "is_active": true, + "is_admin": null, + "is_invited": true, + "prefs": {}, + "writable_by": [ + "ardev-tpzed-000000000000000", + "ardev-tpzed-q6dvn7sby55up1b", + "ardev-j7d0g-000000000000000" + ] +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/users-list.json b/src/test/resources/org/arvados/client/api/client/users-list.json new file mode 100644 index 0000000000..2ff1ded00f --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/users-list.json @@ -0,0 +1,115 @@ +{ + "kind": "arvados#userList", + "etag": "", + "self_link": "", + "offset": 0, + "limit": 100, + "items": [ + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-12389ux30402est", + "email": "test.user@email.com", + "first_name": "Test", + "last_name": "User", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-123vn7sby55up1b", + "email": "test.user1@email.com", + "first_name": "Test1", + "last_name": "User1", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-123g70lq1m3c6fz", + "email": "test.user2@email.com", + "first_name": "Test2", + "last_name": "User2", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-1233zsoudkgq92e", + "email": "test.user3@email.com", + "first_name": "Test3", + "last_name": "User3", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-1234xjvs0clppd3", + "email": "test.user4@email.com", + "first_name": "Test4", + "last_name": "User4", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-123bpggscmn6z8m", + "email": "test.user5@email.com", + "first_name": "Test5", + "last_name": "User5", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-1231uysivaz6ipi", + "email": "test.user6@email.com", + "first_name": "Test6", + "last_name": "User6", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-123b0a1wu0q6cm4", + "email": "test.user7@email.com", + "first_name": "Test7", + "last_name": "User7", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-123bz6n6si24t6v", + "email": "test.user8@email.com", + "first_name": "Test8", + "last_name": "User8", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-123lxhzifligheu", + "email": "test.user9@email.com", + "first_name": "Test9", + "last_name": "User9", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-123gaz31qbopewh", + "email": "test.user10@email.com", + "first_name": "Test10", + "last_name": "User10", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-123dmcf65z973uo", + "email": "test.user11@email.com", + "first_name": "Test11", + "last_name": "User11", + "is_active": true + }, + { + "kind": "arvados#user", + "uuid": "ardev-tpzed-1239y3lj7ybpyg8", + "email": "test.user12@email.com", + "first_name": "Test12", + "last_name": "User12", + "is_active": true + } + + ], + "items_available": 13 +} \ No newline at end of file diff --git a/src/test/resources/org/arvados/client/api/client/users-system.json b/src/test/resources/org/arvados/client/api/client/users-system.json new file mode 100644 index 0000000000..38441c588d --- /dev/null +++ b/src/test/resources/org/arvados/client/api/client/users-system.json @@ -0,0 +1,24 @@ +{ + "href": "/users/ardev-tpzed-000000000000000", + "kind": "arvados#user", + "etag": "2ehmra38iwfuexvz1cjno5xua", + "uuid": "ardev-tpzed-000000000000000", + "owner_uuid": "ardev-tpzed-000000000000000", + "created_at": "2016-10-19T07:48:04.838534000Z", + "modified_by_client_uuid": null, + "modified_by_user_uuid": "ardev-tpzed-000000000000000", + "modified_at": "2016-10-19T07:48:04.833164000Z", + "email": "root", + "username": null, + "full_name": "root", + "first_name": "root", + "last_name": "", + "identity_url": null, + "is_active": true, + "is_admin": true, + "is_invited": true, + "prefs": {}, + "writable_by": [ + "ardev-tpzed-000000000000000" + ] +} \ No newline at end of file diff --git a/src/test/resources/selfsigned.keystore.jks b/src/test/resources/selfsigned.keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..86b126af2e3b9d59f07200a20357aaf369f55533 GIT binary patch literal 2247 zcmchYX*3iHAI4|K%wWbamP@uVM)nyqgUK#M_DYt*4Uv7BWVy0jjd2N)iX@f7L`2Ba zAUiX-wnHSTjHPR2YZ_~fu5-`(zUQ9P_xHo|;s5*eKhJs2^X#wguL1x7(1C#e2<9Cc zW$LN-OtF$u#w^1RN4!FEerP`XX;{0%k+f!hD>zY`YKb-^Mie{`pEOfu0M z`^tqw_LRy$EOwmf5sBiEX5j zZE+xuhQY6red|UExAw(KR7LRZ?V|x=&WB8`@Y?lutbkhg*Ax7cLZ@vqVG3-_YyN{_3Rd8`acZSnYrDrQfYb*W&H2O8~PJ zp)Y=ij%?4r(G05#A6*`8>`9{J`aQFJ-3xXvXSJJz2@0x1`nK(sI#znRDb8H`4rbaZ zIju?*l|D`kmmWG#^D>^k*|1p)vD=G9b6;$2MNfLrp>>@D-WkuBAb^-esvfyQz130! z9wpQ1$i7^<_uT08i5vV8=}sJLk*YW))qP~*dwzIIQZR3+c8EVrC%QzYRTuj~;u}|y z=Iz;z-$hpUwEn(B8DWIV!)-)Rp3M!J6siw758u@bN16CFG4c111*haGXkU8zD0t2^ zPYHv)u0dvcP11j0E_fEhjz69gkDStKlf{2s=GkR8!{;2L78k4w^bkO*^%0Bk#;yqq z_vIRo#i03yH*BY1=(@!;7i+hgwV)cpeWwt&=q#&RisNKxo>EsHSMQEFmZsw_i|^Qo z^$?fWTjcbp)u2LyUNXxmcd8dt2q)AoZK_PjZ?5Tw8f&FIkyILtNP$}xR+#^|f*GtL z2reX^U_7>c|E15&#wydzjF->BP+FQ+?{BCT72gWBs#?gsa2OeQ-X=+hDEoFpXm%Wz zpfu^T*=(d?d790yYEZa?L}hf<3@s^Z%WxLj*S_(^>>)#BvY*E1vpx@)xtI}c9}=qB zYU`Zr@v~8|#P*n{(#28d5^nrBv%d9yi&D4xB|1&{(CG%Ew|Xax1z#efRbk&NMY4V8 zS$?WYHR#qm-NLW@MSX>`z(OIhHyJCK^TOkL$7Wg$`*Q5rCZ0mBSk1aQ4N?^$6}@-v z)u(M2Sq+amV~8Q7^!a0fZaI^T1mhiF!N{2H+L@MfIQOih=#f*05mUnscMF$r*bG{7 zpPG8!lNCj3Fh@aOx{a+{KZ&>4K;(psQE;@>fvU~|dSgxL%m;IPjVGAVY??Ij(zQ%? zT6!%e@O6=R8&20oh|hvmVII>%5A3Idr+us_!dB@5p>p-ozUxFCI9S6K76GBJ~(_z5cW!50?Y)Qh*+a+y-)_=+o;;Ka=7ytl# z@dPSg41o&rddvp`fp9uUX!uLNyKuIBp`h+7yZ6c9mKqR?KA#_PZg0}vD&j0f_R3PTxb{vca z9~CG9AW(rY5ETdnbVS3l%eLn8?vIRjI%~}6puW*Ql`t94-7GjZR)47f@FR4`U9Hgc zq^U9j9*Pj@zioWHX8jKJL-@T}Yh#4^%2$Rg+HLI0(CULowm2HObxO#)NdNmiqvO$D z6>SgfhUxb`l%6OGjSFW5w8=Ga2Oac-ssxf6mKEju-`p}#A_wM7qwZ#khFpY7ddL)0 zB$Tt7eS6W@K29+GM4dl7$aFB+=%f1dR7Zr^tf}X4)J*YHi_B=Fgigo8W&Y10?AI{d zxW+@eH}O2Poxo1GIL(vFgLvrU zdD0P5v1@i85yS@s0811J3IzFsgvbknCBYJ(&qf+ma#|=W)F#!C-jA2ZxYE#nn|P3^ z0~3J&>utxd9wO)AqsiMu^*2Pn&0}JEA4peBQu$wr6#!C9g6xbQnquBfc&=;?(X(L< zLMLwzRCKxvTk3m1NQw*%ZA}Ry9XDXKTIu|JIp!;mF~d_FIfFvT0SMDR{FM(pyNfUNu6;c|)Y?Oz(53(+b3 z9kf(|5i!w$%_hOT30w4`fpSLaifIl{UCyJ8O*^a5i&*eAhRKxaKwUdp)D)}w-LEda XNG=48