From 0b7ad46353970d18d76e7f42fa6eff7f52582690 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Tue, 7 Jun 2016 17:13:14 -0700 Subject: [PATCH 01/27] Done storage usages --- build-tools/src/main/resources/suppressions.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build-tools/src/main/resources/suppressions.xml b/build-tools/src/main/resources/suppressions.xml index 690bb7a9fc35f..becdbdeb751d3 100644 --- a/build-tools/src/main/resources/suppressions.xml +++ b/build-tools/src/main/resources/suppressions.xml @@ -31,6 +31,7 @@ --> - + + \ No newline at end of file From 9ab4ca6e121b26d28df2bf204891cab07881f202 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 8 Jun 2016 15:34:03 -0700 Subject: [PATCH 02/27] Network passes checkstyle --- build-tools/src/main/resources/checkstyle.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/build-tools/src/main/resources/checkstyle.xml b/build-tools/src/main/resources/checkstyle.xml index d156ff63e65f6..1875d6f100cab 100644 --- a/build-tools/src/main/resources/checkstyle.xml +++ b/build-tools/src/main/resources/checkstyle.xml @@ -248,7 +248,6 @@ --> - From 1bc2ab88a4c6f80dce75d256b6aff722e0958d13 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Fri, 10 Jun 2016 16:38:28 -0700 Subject: [PATCH 03/27] Checkstyle passes everywhere --- build-tools/src/main/resources/suppressions.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/build-tools/src/main/resources/suppressions.xml b/build-tools/src/main/resources/suppressions.xml index becdbdeb751d3..29e5bd66eefb5 100644 --- a/build-tools/src/main/resources/suppressions.xml +++ b/build-tools/src/main/resources/suppressions.xml @@ -31,6 +31,7 @@ --> + From 618091bdc732b5b3884162bebdda201b748b3a54 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Tue, 14 Jun 2016 17:03:29 -0700 Subject: [PATCH 04/27] Basic prototyping of async in fluent --- .../java/com/microsoft/azure/TaskGroup.java | 16 +++++ .../com/microsoft/azure/TaskGroupBase.java | 71 ++++++++++++++----- .../java/com/microsoft/azure/TaskItem.java | 9 ++- 3 files changed, 76 insertions(+), 20 deletions(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java index 94b4628ff70ae..29507425d4439 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java @@ -7,6 +7,9 @@ package com.microsoft.azure; +import com.microsoft.rest.ServiceCall; +import com.microsoft.rest.ServiceCallback; + /** * Represents a group of related tasks. *

@@ -39,6 +42,11 @@ public interface TaskGroup> { */ void merge(TaskGroup parentTaskGroup); + /** + * Prepare the graph for execution. + */ + void prepare(); + /** * Executes the tasks in the group. *

@@ -48,6 +56,14 @@ public interface TaskGroup> { */ void execute() throws Exception; + /** + * Executes the tasks in the group asynchronously. + * + * @param callback the callback to call on failure or success + * @return the handle to the REST call + */ + ServiceCall executeAsync(ServiceCallback callback); + /** * Gets the result of execution of a task in the group. *

diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java index d0dc430edc3a6..a4a93ae1f281c 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java @@ -7,15 +7,17 @@ package com.microsoft.azure; +import com.microsoft.rest.ServiceCall; +import com.microsoft.rest.ServiceCallback; + /** * The base implementation of TaskGroup interface. * * @param the result type of the tasks in the group - * @param type representing task in the group */ -public abstract class TaskGroupBase> - implements TaskGroup { - private DAGraph> dag; +public abstract class TaskGroupBase + implements TaskGroup> { + private DAGraph, DAGNode>> dag; /** * Creates TaskGroupBase. @@ -23,12 +25,12 @@ public abstract class TaskGroupBase> * @param rootTaskItemId the id of the root task in this task group * @param rootTaskItem the root task */ - public TaskGroupBase(String rootTaskItemId, U rootTaskItem) { + public TaskGroupBase(String rootTaskItemId, TaskItem rootTaskItem) { this.dag = new DAGraph<>(new DAGNode<>(rootTaskItemId, rootTaskItem)); } @Override - public DAGraph> dag() { + public DAGraph, DAGNode>> dag() { return dag; } @@ -38,24 +40,42 @@ public boolean isRoot() { } @Override - public void merge(TaskGroup parentTaskGroup) { + public void merge(TaskGroup> parentTaskGroup) { dag.merge(parentTaskGroup.dag()); } @Override - public void execute() throws Exception { + public void prepare() { if (isRoot()) { dag.prepare(); - DAGNode nextNode = dag.getNext(); - while (nextNode != null) { - if (dag.isRootNode(nextNode)) { - executeRootTask(nextNode.data()); - } else { - nextNode.data().execute(); - } - dag.reportedCompleted(nextNode); - nextNode = dag.getNext(); - } + } + } + + @Override + public void execute() throws Exception { + DAGNode> nextNode = dag.getNext(); + if (nextNode == null) { + return; + } + + if (dag.isRootNode(nextNode)) { + executeRootTask(nextNode.data()); + } else { + nextNode.data().execute(this, nextNode); + } + } + + @Override + public ServiceCall executeAsync(final ServiceCallback callback) { + final DAGNode> nextNode = dag.getNext(); + if (nextNode == null) { + return null; + } + + if (dag.isRootNode(nextNode)) { + return executeRootTaskAsync(nextNode.data(), callback); + } else { + return nextNode.data().executeAsync(this, nextNode, callback); } } @@ -74,5 +94,18 @@ public T taskResult(String taskId) { * @param task the root task in this group * @throws Exception the exception */ - public abstract void executeRootTask(U task) throws Exception; + public abstract void executeRootTask(TaskItem task) throws Exception; + + /** + * executes the root task in this group asynchronously. + *

+ * this method will be invoked when all the task dependencies of the root task are finished + * executing, at this point root task can be executed by consuming the result of tasks it + * depends on. + * + * @param task the root task in this group + * @param callback the callback when the task fails or succeeds + * @return the handle to the REST call + */ + public abstract ServiceCall executeRootTaskAsync(TaskItem task, ServiceCallback callback); } diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java index 7df830f69bb9e..870105917457b 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java @@ -7,6 +7,9 @@ package com.microsoft.azure; +import com.microsoft.rest.ServiceCall; +import com.microsoft.rest.ServiceCallback; + /** * Type representing a task in a task group {@link TaskGroup}. * @@ -22,7 +25,11 @@ public interface TaskItem { * Executes the task. *

* once executed the result will be available through result getter + * + * @param taskGroup the task group dispatching tasks * @throws Exception exception */ - void execute() throws Exception; + void execute(TaskGroup> taskGroup, DAGNode> node) throws Exception; + + ServiceCall executeAsync(TaskGroup> taskGroup, DAGNode> node, ServiceCallback callback); } From b8d6ab8557e22735c1610498a7e761d136ff42e4 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 15 Jun 2016 00:08:26 -0700 Subject: [PATCH 05/27] Add applyAsync() --- .../src/main/java/com/microsoft/azure/TaskItem.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java index 870105917457b..fb74c23845c3b 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java @@ -27,9 +27,20 @@ public interface TaskItem { * once executed the result will be available through result getter * * @param taskGroup the task group dispatching tasks + * @param node the node the task item is associated with * @throws Exception exception */ void execute(TaskGroup> taskGroup, DAGNode> node) throws Exception; + /** + * Executes the task asynchronously. + *

+ * once executed the result will be available through result getter + + * @param taskGroup the task group dispatching tasks + * @param node the node the task item is associated with + * @param callback callback to call on success or failure + * @return the handle of the REST call + */ ServiceCall executeAsync(TaskGroup> taskGroup, DAGNode> node, ServiceCallback callback); } From 91d4bb57fdbf3127b6da07a1c384a6dfab220242 Mon Sep 17 00:00:00 2001 From: anuchan Date: Wed, 15 Jun 2016 10:03:50 -0700 Subject: [PATCH 06/27] Adding few more Azure environments --- .../com/microsoft/azure/AzureEnvironment.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java index 1721e2ff97bb8..1012a1fc09ac9 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java @@ -71,7 +71,25 @@ public AzureEnvironment( "https://login.chinacloudapi.cn/", "https://management.core.chinacloudapi.cn/", true, - "https://management.chinacloudapi.cn"); + "https://management.chinacloudapi.cn/"); + + /** + * Provides the settings for authentication with Azure US Government. + */ + public static final AzureEnvironment AZURE_US_GOVERNMENT = new AzureEnvironment( + "https://login.microsoftonline.com/", + "https://management.core.usgovcloudapi.net/", + true, + "https://management.usgovcloudapi.net/"); + + /** + * Provides the settings for authentication with Azure German Government. + */ + public static final AzureEnvironment AZURE_GERMAN_GOVERNMENT = new AzureEnvironment( + "https://login.microsoftonline.de/", + "https://management.core.cloudapi.de/", + true, + "https://management.microsoftazure.de"); /** * Gets the base URL of the management service. From 281f61cf85c11fb41140856be49b338715120330 Mon Sep 17 00:00:00 2001 From: anuchan Date: Wed, 15 Jun 2016 10:34:31 -0700 Subject: [PATCH 07/27] correcting environment name --- .../src/main/java/com/microsoft/azure/AzureEnvironment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java index 1012a1fc09ac9..8c01b6e1e7af9 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java @@ -83,9 +83,9 @@ public AzureEnvironment( "https://management.usgovcloudapi.net/"); /** - * Provides the settings for authentication with Azure German Government. + * Provides the settings for authentication with Azure German. */ - public static final AzureEnvironment AZURE_GERMAN_GOVERNMENT = new AzureEnvironment( + public static final AzureEnvironment AZURE_GERMAN = new AzureEnvironment( "https://login.microsoftonline.de/", "https://management.core.cloudapi.de/", true, From 012a68bfa71532e7179a8959ccc7fcfa57437f42 Mon Sep 17 00:00:00 2001 From: anuchan Date: Wed, 15 Jun 2016 10:36:13 -0700 Subject: [PATCH 08/27] ensuring trailing slash for urls --- .../src/main/java/com/microsoft/azure/AzureEnvironment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java index 8c01b6e1e7af9..d4374a9f21ffc 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java @@ -89,7 +89,7 @@ public AzureEnvironment( "https://login.microsoftonline.de/", "https://management.core.cloudapi.de/", true, - "https://management.microsoftazure.de"); + "https://management.microsoftazure.de/"); /** * Gets the base URL of the management service. From 1fd9e5f43ea7eac1a8f1b4ce4634f124c9f60313 Mon Sep 17 00:00:00 2001 From: anuchan Date: Wed, 15 Jun 2016 16:23:11 -0700 Subject: [PATCH 09/27] use AZURE_GERMANY insteadof AZURE_GERMAN --- .../src/main/java/com/microsoft/azure/AzureEnvironment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java index d4374a9f21ffc..1b4ec58f12c7c 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java @@ -83,9 +83,9 @@ public AzureEnvironment( "https://management.usgovcloudapi.net/"); /** - * Provides the settings for authentication with Azure German. + * Provides the settings for authentication with Azure Germany. */ - public static final AzureEnvironment AZURE_GERMAN = new AzureEnvironment( + public static final AzureEnvironment AZURE_GERMANY = new AzureEnvironment( "https://login.microsoftonline.de/", "https://management.core.cloudapi.de/", true, From 8604deeccbfa0904f5326e7f8a782a9367410a2c Mon Sep 17 00:00:00 2001 From: anuchan Date: Thu, 16 Jun 2016 13:09:47 -0700 Subject: [PATCH 10/27] Fixing the bug of trying to create created resources during update, adding better way to name gen resource names --- .../src/main/java/com/microsoft/azure/TaskGroupBase.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java index d0dc430edc3a6..1a43baebf0004 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java @@ -50,10 +50,15 @@ public void execute() throws Exception { while (nextNode != null) { if (dag.isRootNode(nextNode)) { executeRootTask(nextNode.data()); + dag.reportedCompleted(nextNode); } else { - nextNode.data().execute(); + // TaskGroupBase::execute will be called both in update and create + // scenarios, so run the task only if it not not executed already. + if (nextNode.data().result() == null) { + nextNode.data().execute(); + dag.reportedCompleted(nextNode); + } } - dag.reportedCompleted(nextNode); nextNode = dag.getNext(); } } From 5e5647fd87c1eecaed069447fd86c2ceb7a42cee Mon Sep 17 00:00:00 2001 From: anuchan Date: Thu, 16 Jun 2016 14:18:18 -0700 Subject: [PATCH 11/27] making dag::prepare to work for update --- .../src/main/java/com/microsoft/azure/DAGNode.java | 8 +++++++- .../src/main/java/com/microsoft/azure/DAGraph.java | 4 ++++ .../src/main/java/com/microsoft/azure/TaskGroupBase.java | 3 +-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java index ccc700618e58f..7de859e25f1f6 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java @@ -59,7 +59,6 @@ public List dependencyKeys() { * @param dependencyKey the id of the dependency node */ public void addDependency(String dependencyKey) { - toBeResolved++; super.addChild(dependencyKey); } @@ -70,6 +69,13 @@ public boolean hasDependencies() { return this.hasChildren(); } + /** + * prepare the node for traversal. + */ + public void prepare() { + this.toBeResolved = this.dependencyKeys().size(); + } + /** * @return true if all dependencies of this node are ready to be consumed */ diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java index 8b10bde8b3e3c..cfd50d9ca5b40 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java @@ -77,6 +77,10 @@ public void merge(DAGraph parent) { * in the DAG with no dependencies. */ public void prepare() { + for (Map.Entry entry: graph.entrySet()) { + entry.getValue().prepare(); + } + initializeQueue(); if (queue.isEmpty()) { throw new RuntimeException("Found circular dependency"); diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java index 1a43baebf0004..5cf4369142ea3 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java @@ -50,15 +50,14 @@ public void execute() throws Exception { while (nextNode != null) { if (dag.isRootNode(nextNode)) { executeRootTask(nextNode.data()); - dag.reportedCompleted(nextNode); } else { // TaskGroupBase::execute will be called both in update and create // scenarios, so run the task only if it not not executed already. if (nextNode.data().result() == null) { nextNode.data().execute(); - dag.reportedCompleted(nextNode); } } + dag.reportedCompleted(nextNode); nextNode = dag.getNext(); } } From 22f2053749b65feaf4444a4a5b86083091f10d01 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Fri, 17 Jun 2016 10:15:46 -0700 Subject: [PATCH 12/27] Add a bunch of configs to RestClient --- .../java/com/microsoft/azure/RestClient.java | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java b/azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java index 2517f890a52ca..532ba2e392809 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java @@ -14,17 +14,20 @@ import com.microsoft.rest.credentials.ServiceClientCredentials; import com.microsoft.rest.retry.RetryHandler; import com.microsoft.rest.serializer.JacksonMapperAdapter; - -import java.lang.reflect.Field; -import java.net.CookieManager; -import java.net.CookiePolicy; - +import okhttp3.ConnectionPool; import okhttp3.Interceptor; import okhttp3.JavaNetCookieJar; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; +import java.lang.reflect.Field; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.Proxy; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + /** * An instance of this class stores the client information for making REST calls. */ @@ -260,6 +263,63 @@ public Buildable withInterceptor(Interceptor interceptor) { return this; } + /** + * Set the read timeout on the HTTP client. Default is 10 seconds. + * + * @param timeout the timeout numeric value + * @param unit the time unit for the numeric value + * @return the builder itself for chaining + */ + public Buildable withReadTimeout(long timeout, TimeUnit unit) { + httpClientBuilder.readTimeout(timeout, unit); + return this; + } + + /** + * Set the connection timeout on the HTTP client. Default is 10 seconds. + * + * @param timeout the timeout numeric value + * @param unit the time unit for the numeric value + * @return the builder itself for chaining + */ + public Buildable withConnectionTimeout(long timeout, TimeUnit unit) { + httpClientBuilder.connectTimeout(timeout, unit); + return this; + } + + /** + * Set the maximum idle connections for the HTTP client. Default is 5. + * + * @param maxIdleConnections the maximum idle connections + * @return the builder itself for chaining + */ + public Buildable withMaxIdleConnections(int maxIdleConnections) { + httpClientBuilder.connectionPool(new ConnectionPool(maxIdleConnections, 5, TimeUnit.MINUTES)); + return this; + } + + /** + * Sets the executor for async callbacks to run on. + * + * @param executor the executor to execute the callbacks. + * @return the builder itself for chaining + */ + public Buildable withCallbackExecutor(Executor executor) { + retrofitBuilder.callbackExecutor(executor); + return this; + } + + /** + * Sets the proxy for the HTTP client. + * + * @param proxy the proxy to use + * @return the builder itself for chaining + */ + public Buildable withProxy(Proxy proxy) { + httpClientBuilder.proxy(proxy); + return this; + } + /** * Build a RestClient with all the current configurations. * From 99abc58ed0a9db124e47e5bdccebe0d542251a42 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Fri, 17 Jun 2016 11:19:27 -0700 Subject: [PATCH 13/27] Add retry for 404 GET --- ...rceGetExponentialBackoffRetryStrategy.java | 48 +++++++++++++++++++ .../java/com/microsoft/azure/RestClient.java | 1 + 2 files changed, 49 insertions(+) create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/ResourceGetExponentialBackoffRetryStrategy.java diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/ResourceGetExponentialBackoffRetryStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/ResourceGetExponentialBackoffRetryStrategy.java new file mode 100644 index 0000000000000..a145fecc6795b --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/ResourceGetExponentialBackoffRetryStrategy.java @@ -0,0 +1,48 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.retry.RetryStrategy; +import okhttp3.Response; + +/** + * A retry strategy with backoff parameters for calculating the exponential + * delay between retries for 404s from GET calls. + */ +public class ResourceGetExponentialBackoffRetryStrategy extends RetryStrategy { + /** + * Represents the default number of retries. + */ + private static final int DEFAULT_NUMBER_OF_ATTEMPTS = 3; + + /** + * Creates an instance of the retry strategy. + */ + public ResourceGetExponentialBackoffRetryStrategy() { + this(null, DEFAULT_FIRST_FAST_RETRY); + } + + /** + * Initializes a new instance of the {@link RetryStrategy} class. + * + * @param name The name of the retry strategy. + * @param firstFastRetry true to immediately retry in the first attempt; otherwise, false. + */ + private ResourceGetExponentialBackoffRetryStrategy(String name, boolean firstFastRetry) { + super(name, firstFastRetry); + } + + @Override + public boolean shouldRetry(int retryCount, Response response) { + int code = response.code(); + //CHECKSTYLE IGNORE MagicNumber FOR NEXT 2 LINES + return retryCount < DEFAULT_NUMBER_OF_ATTEMPTS + && code == 404 + && response.request().method().equalsIgnoreCase("GET"); + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java b/azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java index 532ba2e392809..0ace881f0ee51 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java @@ -330,6 +330,7 @@ public RestClient build() { OkHttpClient httpClient = httpClientBuilder .addInterceptor(baseUrlHandler) .addInterceptor(customHeadersInterceptor) + .addInterceptor(new RetryHandler(new ResourceGetExponentialBackoffRetryStrategy())) .addInterceptor(new RetryHandler()) .build(); return new RestClient(httpClient, From a56b0451109fe0434e525931683bb24a24c712a3 Mon Sep 17 00:00:00 2001 From: anuchan Date: Tue, 21 Jun 2016 12:46:20 -0700 Subject: [PATCH 14/27] couple of javadoc fixes --- .../credentials/ApplicationTokenCredentials.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/ApplicationTokenCredentials.java b/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/ApplicationTokenCredentials.java index 163d10f60a1d5..384ab3f7671af 100644 --- a/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/ApplicationTokenCredentials.java +++ b/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/ApplicationTokenCredentials.java @@ -113,13 +113,14 @@ public ApplicationTokenCredentials withDefaultSubscriptionId(String subscription * * @param credentialsFile A file with credentials, using the standard Java properties format. * and the following keys: - * subscription= - * tenant= - * client= - * key= - * managementURI= - * baseURL= - * authURL= + * subscription=<subscription-id> + * tenant=<tenant-id> + * client=<client-id> + * key=<client-key> + * managementURI=<management-URI> + * baseURL=<base-URL> + * authURL=<authentication-URL> + * * @return The credentials based on the file. * @throws IOException exception thrown from file access errors. */ From c841500cf9e1484d87fb6c533feee6558e95b4b5 Mon Sep 17 00:00:00 2001 From: anuchan Date: Wed, 22 Jun 2016 11:00:39 -0700 Subject: [PATCH 15/27] Making DAG to work correctly to handle more cases --- .../java/com/microsoft/azure/DAGNode.java | 25 ++++++++++-- .../java/com/microsoft/azure/DAGraph.java | 40 ++++++++++++------- .../main/java/com/microsoft/azure/Node.java | 3 +- .../java/com/microsoft/azure/TaskGroup.java | 11 ++--- .../com/microsoft/azure/TaskGroupBase.java | 20 +++------- .../java/com/microsoft/azure/DAGraphTest.java | 2 +- .../com/microsoft/azure/DAGraphTests.java | 2 +- 7 files changed, 63 insertions(+), 40 deletions(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java index 7de859e25f1f6..890228fff654c 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java @@ -8,6 +8,7 @@ package com.microsoft.azure; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -18,6 +19,7 @@ public class DAGNode extends Node { private List dependentKeys; private int toBeResolved; + private boolean isPreparer; /** * Creates a DAG node. @@ -34,7 +36,7 @@ public DAGNode(String key, T data) { * @return a list of keys of nodes in {@link DAGraph} those are dependents on this node */ List dependentKeys() { - return this.dependentKeys; + return Collections.unmodifiableList(this.dependentKeys); } /** @@ -70,10 +72,27 @@ public boolean hasDependencies() { } /** - * prepare the node for traversal. + * Mark or un-mark this node as being preparer. + * + * @param isPreparer true if this node needs to be marked as preparer, false otherwise. + */ + public void setPreparer(boolean isPreparer) { + this.isPreparer = isPreparer; + } + + /** + * @return true if this node is marked as preparer + */ + public boolean isPreparer() { + return isPreparer; + } + + /** + * initialize the node so that traversal can be performed on the parent DAG. */ - public void prepare() { + public void initialize() { this.toBeResolved = this.dependencyKeys().size(); + this.dependentKeys.clear(); } /** diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java index cfd50d9ca5b40..6bb33b0e0b2c7 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java @@ -32,6 +32,7 @@ public class DAGraph> extends Graph { public DAGraph(U rootNode) { this.rootNode = rootNode; this.queue = new ArrayDeque<>(); + this.rootNode.setPreparer(true); this.addNode(rootNode); } @@ -52,6 +53,14 @@ public boolean isRootNode(U node) { return this.rootNode == node; } + /** + * @return true if this dag is the preparer responsible for + * preparing the DAG for traversal. + */ + public boolean isPreparer() { + return this.rootNode.isPreparer(); + } + /** * Merge this DAG with another DAG. *

@@ -63,7 +72,6 @@ public boolean isRootNode(U node) { public void merge(DAGraph parent) { this.hasParent = true; parent.rootNode.addDependency(this.rootNode.key()); - this.rootNode.addDependent(parent.rootNode.key()); for (Map.Entry entry: graph.entrySet()) { String key = entry.getKey(); if (!parent.graph.containsKey(key)) { @@ -77,13 +85,15 @@ public void merge(DAGraph parent) { * in the DAG with no dependencies. */ public void prepare() { - for (Map.Entry entry: graph.entrySet()) { - entry.getValue().prepare(); - } - - initializeQueue(); - if (queue.isEmpty()) { - throw new RuntimeException("Found circular dependency"); + if (isPreparer()) { + for (Map.Entry entry : graph.entrySet()) { + // Prepare each node for traversal + entry.getValue().initialize(); + // Mark other sub-DAGs are non-preparer + entry.getValue().setPreparer(false); + } + initializeDependentKeys(); + initializeQueue(); } } @@ -115,6 +125,7 @@ public T getNodeData(String key) { * @param completed the node ready to be consumed */ public void reportedCompleted(U completed) { + completed.setPreparer(true); String dependency = completed.key(); for (String dependentKey : graph.get(dependency).dependentKeys()) { DAGNode dependent = graph.get(dependentKey); @@ -126,27 +137,25 @@ public void reportedCompleted(U completed) { } /** - * populate dependents of all nodes. + * Initializes dependents of all nodes. *

* the DAG will be explored in DFS order and all node's dependents will be identified, * this prepares the DAG for traversal using getNext method, each call to getNext returns next node * in the DAG with no dependencies. */ - public void populateDependentKeys() { - this.queue.clear(); + private void initializeDependentKeys() { visit(new Visitor() { + // This 'visit' will be called only once per each node. @Override public void visit(U node) { if (node.dependencyKeys().isEmpty()) { - queue.add(node.key()); return; } String dependentKey = node.key(); for (String dependencyKey : node.dependencyKeys()) { graph.get(dependencyKey) - .dependentKeys() - .add(dependentKey); + .addDependent(dependentKey); } } }); @@ -163,5 +172,8 @@ private void initializeQueue() { this.queue.add(entry.getKey()); } } + if (queue.isEmpty()) { + throw new RuntimeException("Found circular dependency"); + } } } diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/Node.java b/azure-client-runtime/src/main/java/com/microsoft/azure/Node.java index 3dc61d8065f6e..cbb83f1d2a58f 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/Node.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/Node.java @@ -8,6 +8,7 @@ package com.microsoft.azure; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -57,7 +58,7 @@ public boolean hasChildren() { * @return children (neighbours) of this node */ public List children() { - return this.children; + return Collections.unmodifiableList(this.children); } /** diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java index 29507425d4439..26bbbece2606b 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java @@ -27,11 +27,6 @@ public interface TaskGroup> { */ DAGraph> dag(); - /** - * @return true if this is a root (parent) task group composing other task groups. - */ - boolean isRoot(); - /** * Merges this task group with parent task group. *

@@ -42,6 +37,12 @@ public interface TaskGroup> { */ void merge(TaskGroup parentTaskGroup); + /** + * @return true if the group is responsible for preparing execution of original task in + * this group and all tasks belong other task group it composes. + */ + boolean isPreparer(); + /** * Prepare the graph for execution. */ diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java index 48978f12ca11b..4144a42af39af 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java @@ -35,8 +35,8 @@ public DAGraph, DAGNode>> dag() { } @Override - public boolean isRoot() { - return !dag.hasParent(); + public boolean isPreparer() { + return dag.isPreparer(); } @Override @@ -46,7 +46,7 @@ public void merge(TaskGroup> parentTaskGroup) { @Override public void prepare() { - if (isRoot()) { + if (isPreparer()) { dag.prepare(); } } @@ -61,11 +61,7 @@ public void execute() throws Exception { if (dag.isRootNode(nextNode)) { executeRootTask(nextNode.data()); } else { - // TaskGroupBase::execute will be called both in update and create - // scenarios, so run the task only if it not not executed already. - if (nextNode.data().result() == null) { - nextNode.data().execute(this, nextNode); - } + nextNode.data().execute(this, nextNode); } } @@ -79,13 +75,7 @@ public ServiceCall executeAsync(final ServiceCallback callback) { if (dag.isRootNode(nextNode)) { return executeRootTaskAsync(nextNode.data(), callback); } else { - // TaskGroupBase::execute will be called both in update and create - // scenarios, so run the task only if it not not executed already. - if (nextNode.data().result() == null) { - return nextNode.data().executeAsync(this, nextNode, callback); - } else { - return null; - } + return nextNode.data().executeAsync(this, nextNode, callback); } } diff --git a/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTest.java b/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTest.java index 8c1e12dd7011d..f3b278797e2dd 100644 --- a/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTest.java +++ b/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTest.java @@ -64,7 +64,7 @@ public void testDAGraphGetNext() { dag.addNode(nodeH); dag.addNode(nodeI); - dag.populateDependentKeys(); + dag.prepare(); DAGNode nextNode = dag.getNext(); int i = 0; while (nextNode != null) { diff --git a/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTests.java b/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTests.java index e368b04c89db4..985fe306f0f52 100644 --- a/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTests.java +++ b/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTests.java @@ -71,7 +71,7 @@ public void testDAGraphGetNext() { dag.addNode(nodeH); dag.addNode(nodeI); - dag.populateDependentKeys(); + dag.prepare(); DAGNode nextNode = dag.getNext(); int i = 0; while (nextNode != null) { From 4688b887f1af7b569500c3058c008d2737c116f5 Mon Sep 17 00:00:00 2001 From: anuchan Date: Wed, 22 Jun 2016 11:19:36 -0700 Subject: [PATCH 16/27] comment corrections --- .../src/main/java/com/microsoft/azure/DAGNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java index 890228fff654c..efaf8a8c8cf95 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java @@ -72,7 +72,7 @@ public boolean hasDependencies() { } /** - * Mark or un-mark this node as being preparer. + * Mark or un-mark this node as preparer. * * @param isPreparer true if this node needs to be marked as preparer, false otherwise. */ From bf4c5eab102f13c5a66832001829aa19d784631c Mon Sep 17 00:00:00 2001 From: anuchan Date: Wed, 22 Jun 2016 16:26:44 -0700 Subject: [PATCH 17/27] Addressing review comments [javadoc, simplfying the map iteration] --- .../src/main/java/com/microsoft/azure/DAGNode.java | 6 +++--- .../src/main/java/com/microsoft/azure/DAGraph.java | 12 ++++++------ .../src/main/java/com/microsoft/azure/Graph.java | 2 +- .../main/java/com/microsoft/azure/TaskGroupBase.java | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java index efaf8a8c8cf95..112130413fd87 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java @@ -40,7 +40,7 @@ List dependentKeys() { } /** - * mark the node identified by the given key as dependent of this node. + * Mark the node identified by the given key as dependent of this node. * * @param key the id of the dependent node */ @@ -56,7 +56,7 @@ public List dependencyKeys() { } /** - * mark the node identified by the given key as this node's dependency. + * Mark the node identified by the given key as this node's dependency. * * @param dependencyKey the id of the dependency node */ @@ -88,7 +88,7 @@ public boolean isPreparer() { } /** - * initialize the node so that traversal can be performed on the parent DAG. + * Initialize the node so that traversal can be performed on the parent DAG. */ public void initialize() { this.toBeResolved = this.dependencyKeys().size(); diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java index 6bb33b0e0b2c7..343e7129e82e6 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java @@ -64,7 +64,7 @@ public boolean isPreparer() { /** * Merge this DAG with another DAG. *

- * this will mark this DAG as a child DAG, the dependencies of nodes in this DAG will be merged + * This will mark this DAG as a child DAG, the dependencies of nodes in this DAG will be merged * with (copied to) the parent DAG * * @param parent the parent DAG @@ -86,11 +86,11 @@ public void merge(DAGraph parent) { */ public void prepare() { if (isPreparer()) { - for (Map.Entry entry : graph.entrySet()) { + for (U node : graph.values()) { // Prepare each node for traversal - entry.getValue().initialize(); + node.initialize(); // Mark other sub-DAGs are non-preparer - entry.getValue().setPreparer(false); + node.setPreparer(false); } initializeDependentKeys(); initializeQueue(); @@ -101,7 +101,7 @@ public void prepare() { * Gets next node in the DAG which has no dependency or all of it's dependencies are resolved and * ready to be consumed. *

- * null will be returned when all the nodes are explored + * Null will be returned when all the nodes are explored * * @return next node */ @@ -139,7 +139,7 @@ public void reportedCompleted(U completed) { /** * Initializes dependents of all nodes. *

- * the DAG will be explored in DFS order and all node's dependents will be identified, + * The DAG will be explored in DFS order and all node's dependents will be identified, * this prepares the DAG for traversal using getNext method, each call to getNext returns next node * in the DAG with no dependencies. */ diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/Graph.java b/azure-client-runtime/src/main/java/com/microsoft/azure/Graph.java index 0cf8e2a7d231b..40ceebaa50b2b 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/Graph.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/Graph.java @@ -15,7 +15,7 @@ /** * Type representing a directed graph data structure. *

- * each node in a graph is represented by {@link Node} + * Each node in a graph is represented by {@link Node} * * @param the type of the data stored in the graph's nodes * @param the type of the nodes in the graph diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java index 4144a42af39af..efc8d30e491bc 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java @@ -85,9 +85,9 @@ public T taskResult(String taskId) { } /** - * executes the root task in this group. + * Executes the root task in this group. *

- * this method will be invoked when all the task dependencies of the root task are finished + * This method will be invoked when all the task dependencies of the root task are finished * executing, at this point root task can be executed by consuming the result of tasks it * depends on. * @@ -97,9 +97,9 @@ public T taskResult(String taskId) { public abstract void executeRootTask(TaskItem task) throws Exception; /** - * executes the root task in this group asynchronously. + * Executes the root task in this group asynchronously. *

- * this method will be invoked when all the task dependencies of the root task are finished + * This method will be invoked when all the task dependencies of the root task are finished * executing, at this point root task can be executed by consuming the result of tasks it * depends on. * From f84bd3d7e19b626a04d94de05d3e47af0e6eeb08 Mon Sep 17 00:00:00 2001 From: anuchan Date: Wed, 22 Jun 2016 16:45:06 -0700 Subject: [PATCH 18/27] improving comment as per review --- .../src/main/java/com/microsoft/azure/DAGraph.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java index 343e7129e82e6..9ec33a692f1aa 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java @@ -100,10 +100,8 @@ public void prepare() { /** * Gets next node in the DAG which has no dependency or all of it's dependencies are resolved and * ready to be consumed. - *

- * Null will be returned when all the nodes are explored * - * @return next node + * @return next node or null if all the nodes have been explored */ public U getNext() { return graph.get(queue.poll()); From 47b9652a002af8e1ac0b09d47f6c425d9fdc8913 Mon Sep 17 00:00:00 2001 From: anuchan Date: Sat, 25 Jun 2016 16:02:07 -0700 Subject: [PATCH 19/27] Adding publicIp sample, only mark sub-DAGs as non-prepare - the root DAG stay as preparer --- .../src/main/java/com/microsoft/azure/DAGraph.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java index 9ec33a692f1aa..58179e0152ed4 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java @@ -89,8 +89,10 @@ public void prepare() { for (U node : graph.values()) { // Prepare each node for traversal node.initialize(); - // Mark other sub-DAGs are non-preparer - node.setPreparer(false); + if (!this.isRootNode(node)) { + // Mark other sub-DAGs as non-preparer + node.setPreparer(false); + } } initializeDependentKeys(); initializeQueue(); From f8a2ad482f05b58bc7cd56bbf14b3360ff182e64 Mon Sep 17 00:00:00 2001 From: anuchan Date: Mon, 27 Jun 2016 19:00:09 -0700 Subject: [PATCH 20/27] Adding javadoc gen as a part of build since javadoc generation catches some of the checkstyle and reference issue --- .../src/main/java/com/microsoft/rest/ServiceClient.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client-runtime/src/main/java/com/microsoft/rest/ServiceClient.java b/client-runtime/src/main/java/com/microsoft/rest/ServiceClient.java index 8311a243de139..5d6f0f850336c 100644 --- a/client-runtime/src/main/java/com/microsoft/rest/ServiceClient.java +++ b/client-runtime/src/main/java/com/microsoft/rest/ServiceClient.java @@ -40,6 +40,9 @@ protected ServiceClient(String baseUrl) { /** * Initializes a new instance of the ServiceClient class. * + * @param baseUrl the service base uri + * @param clientBuilder the http client builder + * @param restBuilder the retrofit rest client builder */ protected ServiceClient(String baseUrl, OkHttpClient.Builder clientBuilder, Retrofit.Builder restBuilder) { if (clientBuilder == null) { From f8d2a9c28c1514f41dec4096a1910325870a0e0e Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Wed, 29 Jun 2016 13:57:15 -0700 Subject: [PATCH 21/27] Bump up version numbers for beta2 release --- azure-android-client-authentication/build.gradle | 4 ++-- azure-client-authentication/build.gradle | 4 ++-- azure-client-authentication/pom.xml | 4 ++-- azure-client-runtime/build.gradle | 4 ++-- azure-client-runtime/pom.xml | 2 +- build-tools/pom.xml | 2 +- client-runtime/build.gradle | 2 +- client-runtime/pom.xml | 2 +- pom.xml | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/azure-android-client-authentication/build.gradle b/azure-android-client-authentication/build.gradle index 2651d8389383e..c557f363b0093 100644 --- a/azure-android-client-authentication/build.gradle +++ b/azure-android-client-authentication/build.gradle @@ -18,7 +18,7 @@ android { minSdkVersion 15 targetSdkVersion 23 versionCode 1 - versionName "1.0.0-SNAPSHOT" + versionName "1.0.0-beta2" } buildTypes { @@ -51,7 +51,7 @@ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.0.1' compile 'com.microsoft.aad:adal:1.1.11' - compile 'com.microsoft.azure:azure-client-runtime:1.0.0-SNAPSHOT' + compile 'com.microsoft.azure:azure-client-runtime:1.0.0-beta2' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-authentication/build.gradle b/azure-client-authentication/build.gradle index 2b40ada004e0e..f22be08ed8d0e 100644 --- a/azure-client-authentication/build.gradle +++ b/azure-client-authentication/build.gradle @@ -10,7 +10,7 @@ buildscript { apply plugin: 'java' apply plugin: 'checkstyle' -version = '1.0.0-SNAPSHOT' +version = '1.0.0-beta2' checkstyle { toolVersion = "6.18" @@ -21,7 +21,7 @@ checkstyle { dependencies { compile 'com.microsoft.azure:adal4j:1.1.2' - compile 'com.microsoft.azure:azure-client-runtime:1.0.0-SNAPSHOT' + compile 'com.microsoft.azure:azure-client-runtime:1.0.0-beta2' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-authentication/pom.xml b/azure-client-authentication/pom.xml index 47582ca1eaa87..ecc305c3ecedd 100644 --- a/azure-client-authentication/pom.xml +++ b/azure-client-authentication/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-SNAPSHOT + 1.0.0-beta2 ../pom.xml @@ -49,7 +49,7 @@ com.microsoft.azure azure-client-runtime - 1.0.0-SNAPSHOT + 1.0.0-beta2 com.microsoft.azure diff --git a/azure-client-runtime/build.gradle b/azure-client-runtime/build.gradle index 5c29359536abc..a65f3dc196dfe 100644 --- a/azure-client-runtime/build.gradle +++ b/azure-client-runtime/build.gradle @@ -10,7 +10,7 @@ buildscript { apply plugin: 'java' apply plugin: 'checkstyle' -version = '1.0.0-SNAPSHOT' +version = '1.0.0-beta2' checkstyle { toolVersion = "6.18" @@ -20,7 +20,7 @@ checkstyle { } dependencies { - compile 'com.microsoft.rest:client-runtime:1.0.0-SNAPSHOT' + compile 'com.microsoft.rest:client-runtime:1.0.0-beta2' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-runtime/pom.xml b/azure-client-runtime/pom.xml index 98d91b43e802e..9bc18f1f83d9f 100644 --- a/azure-client-runtime/pom.xml +++ b/azure-client-runtime/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-SNAPSHOT + 1.0.0-beta2 ../pom.xml diff --git a/build-tools/pom.xml b/build-tools/pom.xml index d540f8f5e6980..e94e7bf4f6ce1 100644 --- a/build-tools/pom.xml +++ b/build-tools/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-SNAPSHOT + 1.0.0-beta2 ../pom.xml diff --git a/client-runtime/build.gradle b/client-runtime/build.gradle index 73750c0d3d935..a633beff14bc3 100644 --- a/client-runtime/build.gradle +++ b/client-runtime/build.gradle @@ -12,7 +12,7 @@ apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'checkstyle' group = 'com.microsoft.rest' -version = '1.0.0-SNAPSHOT' +version = '1.0.0-beta2' checkstyle { toolVersion = "6.18" diff --git a/client-runtime/pom.xml b/client-runtime/pom.xml index e63a421434b38..b8d26493b263f 100644 --- a/client-runtime/pom.xml +++ b/client-runtime/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-SNAPSHOT + 1.0.0-beta2 ../pom.xml diff --git a/pom.xml b/pom.xml index aa77260c5893f..35c9c37b662c2 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-SNAPSHOT + 1.0.0-beta2 pom AutoRest Client Runtimes for Java @@ -140,7 +140,7 @@ com.microsoft.azure autorest-build-tools - 1.0.0-SNAPSHOT + 1.0.0-beta2 com.puppycrawl.tools From 8825f410df12edb8bf94358d796aead4dba97f86 Mon Sep 17 00:00:00 2001 From: alvadb Date: Thu, 30 Jun 2016 11:12:22 -0700 Subject: [PATCH 22/27] Removing check style for hiding utility class constructor. --- build-tools/src/main/resources/checkstyle.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/build-tools/src/main/resources/checkstyle.xml b/build-tools/src/main/resources/checkstyle.xml index 1875d6f100cab..b7f934898253c 100644 --- a/build-tools/src/main/resources/checkstyle.xml +++ b/build-tools/src/main/resources/checkstyle.xml @@ -231,7 +231,6 @@ --> - From 762eaa605c73912aa296575f0c15e358e825f706 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Thu, 30 Jun 2016 13:38:34 -0700 Subject: [PATCH 23/27] Prepare for next development iteration --- azure-android-client-authentication/build.gradle | 4 ++-- azure-client-authentication/build.gradle | 4 ++-- azure-client-authentication/pom.xml | 4 ++-- azure-client-runtime/build.gradle | 4 ++-- azure-client-runtime/pom.xml | 2 +- build-tools/pom.xml | 2 +- client-runtime/build.gradle | 2 +- client-runtime/pom.xml | 2 +- pom.xml | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/azure-android-client-authentication/build.gradle b/azure-android-client-authentication/build.gradle index c557f363b0093..2651d8389383e 100644 --- a/azure-android-client-authentication/build.gradle +++ b/azure-android-client-authentication/build.gradle @@ -18,7 +18,7 @@ android { minSdkVersion 15 targetSdkVersion 23 versionCode 1 - versionName "1.0.0-beta2" + versionName "1.0.0-SNAPSHOT" } buildTypes { @@ -51,7 +51,7 @@ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.0.1' compile 'com.microsoft.aad:adal:1.1.11' - compile 'com.microsoft.azure:azure-client-runtime:1.0.0-beta2' + compile 'com.microsoft.azure:azure-client-runtime:1.0.0-SNAPSHOT' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-authentication/build.gradle b/azure-client-authentication/build.gradle index f22be08ed8d0e..2b40ada004e0e 100644 --- a/azure-client-authentication/build.gradle +++ b/azure-client-authentication/build.gradle @@ -10,7 +10,7 @@ buildscript { apply plugin: 'java' apply plugin: 'checkstyle' -version = '1.0.0-beta2' +version = '1.0.0-SNAPSHOT' checkstyle { toolVersion = "6.18" @@ -21,7 +21,7 @@ checkstyle { dependencies { compile 'com.microsoft.azure:adal4j:1.1.2' - compile 'com.microsoft.azure:azure-client-runtime:1.0.0-beta2' + compile 'com.microsoft.azure:azure-client-runtime:1.0.0-SNAPSHOT' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-authentication/pom.xml b/azure-client-authentication/pom.xml index ecc305c3ecedd..47582ca1eaa87 100644 --- a/azure-client-authentication/pom.xml +++ b/azure-client-authentication/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-beta2 + 1.0.0-SNAPSHOT ../pom.xml @@ -49,7 +49,7 @@ com.microsoft.azure azure-client-runtime - 1.0.0-beta2 + 1.0.0-SNAPSHOT com.microsoft.azure diff --git a/azure-client-runtime/build.gradle b/azure-client-runtime/build.gradle index a65f3dc196dfe..5c29359536abc 100644 --- a/azure-client-runtime/build.gradle +++ b/azure-client-runtime/build.gradle @@ -10,7 +10,7 @@ buildscript { apply plugin: 'java' apply plugin: 'checkstyle' -version = '1.0.0-beta2' +version = '1.0.0-SNAPSHOT' checkstyle { toolVersion = "6.18" @@ -20,7 +20,7 @@ checkstyle { } dependencies { - compile 'com.microsoft.rest:client-runtime:1.0.0-beta2' + compile 'com.microsoft.rest:client-runtime:1.0.0-SNAPSHOT' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-runtime/pom.xml b/azure-client-runtime/pom.xml index 9bc18f1f83d9f..98d91b43e802e 100644 --- a/azure-client-runtime/pom.xml +++ b/azure-client-runtime/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-beta2 + 1.0.0-SNAPSHOT ../pom.xml diff --git a/build-tools/pom.xml b/build-tools/pom.xml index e94e7bf4f6ce1..d540f8f5e6980 100644 --- a/build-tools/pom.xml +++ b/build-tools/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-beta2 + 1.0.0-SNAPSHOT ../pom.xml diff --git a/client-runtime/build.gradle b/client-runtime/build.gradle index a633beff14bc3..73750c0d3d935 100644 --- a/client-runtime/build.gradle +++ b/client-runtime/build.gradle @@ -12,7 +12,7 @@ apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'checkstyle' group = 'com.microsoft.rest' -version = '1.0.0-beta2' +version = '1.0.0-SNAPSHOT' checkstyle { toolVersion = "6.18" diff --git a/client-runtime/pom.xml b/client-runtime/pom.xml index b8d26493b263f..e63a421434b38 100644 --- a/client-runtime/pom.xml +++ b/client-runtime/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-beta2 + 1.0.0-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 35c9c37b662c2..aa77260c5893f 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-beta2 + 1.0.0-SNAPSHOT pom AutoRest Client Runtimes for Java @@ -140,7 +140,7 @@ com.microsoft.azure autorest-build-tools - 1.0.0-beta2 + 1.0.0-SNAPSHOT com.puppycrawl.tools From f037c7972f75f941fcb27645a47d3eebff22d5bd Mon Sep 17 00:00:00 2001 From: Garrett Serack Date: Wed, 6 Jul 2016 14:10:01 -0700 Subject: [PATCH 24/27] Re-organize entire code repo (#1220) * moved Autorest core projects * Moved CSharp generators * Moved nodejs generator * Moved python generator * moved Ruby generator * moved modelers * moved testserver and client runtimes * moved more client runtimes * moved java generator and runtime (and python runtime) * moved nuget package test * Adjusting projects and namespaces -- AzureResourceSchema * adjusted project/namespace name for AutoRest.CSharp.Azure * updated names/namespaces for AutoRest*CSharp projects * updates names/namespaces for extensions * updated more projects and namespaces * path fixes for a lot of stuff * updated namespaces, paths, etc * more updates to get closer to actually running * fix up tests to run in new tree * ensure that dotnet restore runs for whole project * fixed typo * made xprojs work on linux * more fixes to xprojs * Fixed NodeJS test project references * removed superflous file references from njsproj files * updated codegenerator * fixed loading sln files * fixed nuget files * fixed bad paths * fix - actually build package * *sigh* ... restored disabled tests --- .gitignore | 23 + .travis.yml | 14 + LICENSE | 21 + README.md | 22 + .../build.gradle | 117 +++ .../gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53637 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + azure-android-client-authentication/gradlew | 160 ++++ .../gradlew.bat | 90 ++ .../proguard-rules.pro | 17 + .../src/main/AndroidManifest.xml | 8 + .../azure/credentials/AzureEnvironment.java | 86 ++ .../credentials/UserTokenCredentials.java | 190 ++++ .../azure/credentials/package-info.java | 5 + .../src/main/res/values/strings.xml | 3 + azure-client-authentication/build.gradle | 98 ++ azure-client-authentication/pom.xml | 102 ++ .../ApplicationTokenCredentials.java | 224 +++++ .../credentials/UserTokenCredentials.java | 164 ++++ .../azure/credentials/package-info.java | 5 + .../UserTokenCredentialsTests.java | 81 ++ azure-client-runtime/build.gradle | 96 ++ azure-client-runtime/pom.xml | 97 ++ .../microsoft/azure/AzureAsyncOperation.java | 139 +++ .../java/com/microsoft/azure/AzureClient.java | 876 ++++++++++++++++++ .../com/microsoft/azure/AzureEnvironment.java | 139 +++ .../com/microsoft/azure/AzureResponse.java | 51 + .../microsoft/azure/AzureServiceClient.java | 74 ++ .../azure/AzureServiceResponseBuilder.java | 73 ++ .../java/com/microsoft/azure/CloudError.java | 106 +++ .../com/microsoft/azure/CloudException.java | 104 +++ .../azure/CustomHeaderInterceptor.java | 135 +++ .../java/com/microsoft/azure/DAGNode.java | 116 +++ .../java/com/microsoft/azure/DAGraph.java | 179 ++++ .../main/java/com/microsoft/azure/Graph.java | 85 ++ .../azure/ListOperationCallback.java | 96 ++ .../main/java/com/microsoft/azure/Node.java | 70 ++ .../main/java/com/microsoft/azure/Page.java | 31 + .../java/com/microsoft/azure/PagedList.java | 339 +++++++ .../com/microsoft/azure/PollingState.java | 316 +++++++ .../azure/RequestIdHeaderInterceptor.java | 29 + .../java/com/microsoft/azure/Resource.java | 113 +++ ...rceGetExponentialBackoffRetryStrategy.java | 48 + .../java/com/microsoft/azure/RestClient.java | 352 +++++++ .../java/com/microsoft/azure/SubResource.java | 38 + .../java/com/microsoft/azure/TaskGroup.java | 77 ++ .../com/microsoft/azure/TaskGroupBase.java | 111 +++ .../java/com/microsoft/azure/TaskItem.java | 46 + .../com/microsoft/azure/package-info.java | 6 + .../serializer/AzureJacksonMapperAdapter.java | 36 + .../serializer/CloudErrorDeserializer.java | 65 ++ .../azure/serializer/package-info.java | 5 + .../java/com/microsoft/azure/DAGraphTest.java | 145 +++ .../com/microsoft/azure/DAGraphTests.java | 152 +++ .../com/microsoft/azure/PagedListTests.java | 127 +++ .../RequestIdHeaderInterceptorTests.java | 91 ++ build-tools/pom.xml | 47 + build-tools/src/main/resources/checkstyle.xml | 255 +++++ .../src/main/resources/suppressions.xml | 38 + client-runtime/build.gradle | 107 +++ client-runtime/pom.xml | 121 +++ .../com/microsoft/rest/BaseUrlHandler.java | 45 + .../rest/CustomHeadersInterceptor.java | 136 +++ .../com/microsoft/rest/DateTimeRfc1123.java | 80 ++ .../com/microsoft/rest/RestException.java | 46 + .../java/com/microsoft/rest/ServiceCall.java | 65 ++ .../com/microsoft/rest/ServiceCallback.java | 29 + .../com/microsoft/rest/ServiceClient.java | 88 ++ .../com/microsoft/rest/ServiceException.java | 94 ++ .../com/microsoft/rest/ServiceResponse.java | 88 ++ .../rest/ServiceResponseBuilder.java | 289 ++++++ .../rest/ServiceResponseCallback.java | 39 + .../rest/ServiceResponseEmptyCallback.java | 38 + .../rest/ServiceResponseWithHeaders.java | 55 ++ .../microsoft/rest/UserAgentInterceptor.java | 79 ++ .../java/com/microsoft/rest/Validator.java | 124 +++ .../BasicAuthenticationCredentials.java | 60 ++ ...cAuthenticationCredentialsInterceptor.java | 45 + .../credentials/ServiceClientCredentials.java | 23 + .../rest/credentials/TokenCredentials.java | 78 ++ .../TokenCredentialsInterceptor.java | 51 + .../rest/credentials/package-info.java | 5 + .../java/com/microsoft/rest/package-info.java | 6 + .../ExponentialBackoffRetryStrategy.java | 106 +++ .../microsoft/rest/retry/RetryHandler.java | 84 ++ .../microsoft/rest/retry/RetryStrategy.java | 82 ++ .../microsoft/rest/retry/package-info.java | 5 + .../rest/serializer/ByteArraySerializer.java | 41 + .../rest/serializer/CollectionFormat.java | 63 ++ .../serializer/DateTimeRfc1123Serializer.java | 43 + .../rest/serializer/DateTimeSerializer.java | 46 + .../serializer/FlatteningDeserializer.java | 110 +++ .../rest/serializer/FlatteningSerializer.java | 147 +++ .../rest/serializer/HeadersSerializer.java | 49 + .../serializer/JacksonConverterFactory.java | 122 +++ .../rest/serializer/JacksonMapperAdapter.java | 185 ++++ .../rest/serializer/JsonFlatten.java | 25 + .../rest/serializer/package-info.java | 5 + .../com/microsoft/rest/CredentialsTests.java | 71 ++ .../com/microsoft/rest/RetryHandlerTests.java | 74 ++ .../microsoft/rest/ServiceClientTests.java | 59 ++ .../com/microsoft/rest/UserAgentTests.java | 66 ++ .../com/microsoft/rest/ValidatorTests.java | 195 ++++ pom.xml | 220 +++++ 105 files changed, 9930 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 azure-android-client-authentication/build.gradle create mode 100644 azure-android-client-authentication/gradle.properties create mode 100644 azure-android-client-authentication/gradle/wrapper/gradle-wrapper.jar create mode 100644 azure-android-client-authentication/gradle/wrapper/gradle-wrapper.properties create mode 100755 azure-android-client-authentication/gradlew create mode 100644 azure-android-client-authentication/gradlew.bat create mode 100644 azure-android-client-authentication/proguard-rules.pro create mode 100644 azure-android-client-authentication/src/main/AndroidManifest.xml create mode 100644 azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/AzureEnvironment.java create mode 100644 azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/UserTokenCredentials.java create mode 100644 azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/package-info.java create mode 100644 azure-android-client-authentication/src/main/res/values/strings.xml create mode 100644 azure-client-authentication/build.gradle create mode 100644 azure-client-authentication/pom.xml create mode 100644 azure-client-authentication/src/main/java/com/microsoft/azure/credentials/ApplicationTokenCredentials.java create mode 100644 azure-client-authentication/src/main/java/com/microsoft/azure/credentials/UserTokenCredentials.java create mode 100644 azure-client-authentication/src/main/java/com/microsoft/azure/credentials/package-info.java create mode 100644 azure-client-authentication/src/test/java/com/microsoft/azure/credentials/UserTokenCredentialsTests.java create mode 100644 azure-client-runtime/build.gradle create mode 100644 azure-client-runtime/pom.xml create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/AzureAsyncOperation.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/AzureClient.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/AzureResponse.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/AzureServiceClient.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/AzureServiceResponseBuilder.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/CloudError.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/CloudException.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/CustomHeaderInterceptor.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/Graph.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/ListOperationCallback.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/Node.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/Page.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/PagedList.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/PollingState.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/RequestIdHeaderInterceptor.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/Resource.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/ResourceGetExponentialBackoffRetryStrategy.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/SubResource.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/package-info.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/serializer/AzureJacksonMapperAdapter.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/serializer/CloudErrorDeserializer.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/serializer/package-info.java create mode 100644 azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTest.java create mode 100644 azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTests.java create mode 100644 azure-client-runtime/src/test/java/com/microsoft/azure/PagedListTests.java create mode 100644 azure-client-runtime/src/test/java/com/microsoft/azure/RequestIdHeaderInterceptorTests.java create mode 100644 build-tools/pom.xml create mode 100644 build-tools/src/main/resources/checkstyle.xml create mode 100644 build-tools/src/main/resources/suppressions.xml create mode 100644 client-runtime/build.gradle create mode 100644 client-runtime/pom.xml create mode 100644 client-runtime/src/main/java/com/microsoft/rest/BaseUrlHandler.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/CustomHeadersInterceptor.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/DateTimeRfc1123.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/RestException.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/ServiceCall.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/ServiceCallback.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/ServiceClient.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/ServiceException.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/ServiceResponse.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/ServiceResponseBuilder.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/ServiceResponseCallback.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/ServiceResponseEmptyCallback.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/ServiceResponseWithHeaders.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/UserAgentInterceptor.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/Validator.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/credentials/BasicAuthenticationCredentials.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/credentials/BasicAuthenticationCredentialsInterceptor.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/credentials/ServiceClientCredentials.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/credentials/TokenCredentials.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/credentials/TokenCredentialsInterceptor.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/credentials/package-info.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/package-info.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/retry/ExponentialBackoffRetryStrategy.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/retry/RetryHandler.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/retry/RetryStrategy.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/retry/package-info.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/ByteArraySerializer.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/CollectionFormat.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/DateTimeRfc1123Serializer.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/DateTimeSerializer.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/FlatteningDeserializer.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/FlatteningSerializer.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/HeadersSerializer.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonConverterFactory.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonMapperAdapter.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/JsonFlatten.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/package-info.java create mode 100644 client-runtime/src/test/java/com/microsoft/rest/CredentialsTests.java create mode 100644 client-runtime/src/test/java/com/microsoft/rest/RetryHandlerTests.java create mode 100644 client-runtime/src/test/java/com/microsoft/rest/ServiceClientTests.java create mode 100644 client-runtime/src/test/java/com/microsoft/rest/UserAgentTests.java create mode 100644 client-runtime/src/test/java/com/microsoft/rest/ValidatorTests.java create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000..f1c8123eca4b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +build +*.class +target +gen + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ +tmp + +# Package Files # +*.jar +*.war +*.ear +*.MF +.gitrevision +.gradle + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# ide +.idea +*.iml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000000..05c26fa53735e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: android +android: + components: + - build-tools-23.0.1 + - android-23 + - platform-tools + - extra-android-support + - extra-google-m2repository + - extra-android-m2repository +sudo: false +script: + - mvn clean install + - mvn checkstyle:check + - cd ./azure-android-client-authentication && ./gradlew check diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000..4918d653ba68b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Microsoft Azure + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000..dfa96fbf28cbf --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +[![Build Status](https://travis-ci.org/Azure/autorest-clientruntime-for-java.svg?branch=javavnext)](https://travis-ci.org/Azure/autorest-clientruntime-for-java) + +# AutoRest Client Runtimes for Java +The runtime libraries for AutoRest generated Java clients. + +## Repository structure + +### client-runtime +This is the generic runtime. You will need this for AutoRest generated library using Java code generator. + +### azure-client-runtime +This is the runtime with Azure specific customizations. You will need this for AutoRest generated library using Azure.Java code generator. + +### azure-client-authentication +This package provides access to Active Directory authentication on JDK using OrgId or application ID / secret combinations. Multi-factor-auth is currently not supported. + +### azure-android-client-authentication +This package provides access to Active Directory authentication on Android. You can login with Microsoft accounts, OrgId, with or without multi-factor-auth. + +## Build +To build this repository, you will need maven 2.0+ and gradle 1.6+. +Maven is used for [Java SDK](https://github.com/Azure/azure-sdk-for-java) when it's used as a submodule in there. Gradle is used for [AutoRest](https://github.com/Azure/autorest) when it's used as a submodule in there. diff --git a/azure-android-client-authentication/build.gradle b/azure-android-client-authentication/build.gradle new file mode 100644 index 0000000000000..2651d8389383e --- /dev/null +++ b/azure-android-client-authentication/build.gradle @@ -0,0 +1,117 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.3.0' + } +} + +apply plugin: 'com.android.library' +apply plugin: 'maven' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + minSdkVersion 15 + targetSdkVersion 23 + versionCode 1 + versionName "1.0.0-SNAPSHOT" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + + lintOptions { + abortOnError false + } +} + +configurations { + deployerJars +} + +repositories { + mavenCentral() + maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:23.0.1' + compile 'com.microsoft.aad:adal:1.1.11' + compile 'com.microsoft.azure:azure-client-runtime:1.0.0-SNAPSHOT' + testCompile 'junit:junit:4.12' + testCompile 'junit:junit-dep:4.11' + deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" +} + +uploadArchives { + repositories { + mavenDeployer { + configuration = configurations.deployerJars + snapshotRepository(url: "ftp://waws-prod-bay-005.ftp.azurewebsites.windows.net/site/wwwroot/") { + authentication(userName: username, password: password) + } + repository(url: "file://$buildDir/repository") + pom.setArtifactId "azure-android-client-authentication" + pom.project { + name 'Microsoft Azure AutoRest Authentication Library for Java' + description 'This is the authentication library for AutoRest generated Azure Java clients.' + url 'https://github.com/Azure/autorest' + + scm { + url 'scm:git:https://github.com/Azure/AutoRest' + connection 'scm:git:git://github.com/Azure/AutoRest.git' + } + + licenses { + license { + name 'The MIT License (MIT)' + url 'http://opensource.org/licenses/MIT' + distribution 'repo' + } + } + + developers { + developer { + id 'microsoft' + name 'Microsoft' + } + } + } + } + } +} + +task sourcesJar(type: Jar) { + from android.sourceSets.main.java.srcDirs + classifier = 'sources' +} + +task javadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + options.encoding = 'UTF-8' +} + +task javadocJar(type: Jar, dependsOn: [javadoc]) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar + archives javadocJar +} diff --git a/azure-android-client-authentication/gradle.properties b/azure-android-client-authentication/gradle.properties new file mode 100644 index 0000000000000..7311d1b56e3bb --- /dev/null +++ b/azure-android-client-authentication/gradle.properties @@ -0,0 +1,2 @@ +username = fake +password = fake \ No newline at end of file diff --git a/azure-android-client-authentication/gradle/wrapper/gradle-wrapper.jar b/azure-android-client-authentication/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..05ef575b0cd0173fc735f2857ce4bd594ce4f6bd GIT binary patch literal 53637 zcmagFW0a=N(k5EAZR081>auOywr$(CZC96V8(p@my3nWR?C*Rt?>>8Ga;>=U{1Lel zDD75u}rp6Jr1cQuqg>^C$(Gz+VQH zzl8R`GRg|dNs5UotI*4eJ<3i`$w<@DFThLFQO{1#H7hYLv+N%~Ow)}^&dAQtNYVns zT!fjV{VLI->cAu~`&D8zKG=$Lu6gHl?*#n6O!!In&y|7wozULN{2z<@cOKaP;xTtJ zG_f)LKeD3!lhxhH(80mf>HjyxBFMz7_%G|qUn2d_LqzP|?QHA~O~{z&jcp8_oqc0u zVFnqILia4#v}oKIf?(Ie@_rIJ5YzJt+6db~OG;MtX2T-x7Y?I2Uh98n5LS3V1C}HS4FGX~v z$Nc@PV}OL57{$6`F?OZpC3tYw1_6FuD$Mp!j{*rU*hqXn<%A*gByd7vSP+Eau|x2# zbojpicFH5Wp{r|$!G;AH>zuv{!no&WYcJOy1{EKKcOER79a z?4AB~2&Kxl_9%i#ei(r8v4z7*gWA;1RWFs}DEkEi9O&3cXeQYzSs4LaLs0WNcN6=> zhx(^zTh@EXx8j)QAE`vZsJBD2SG2W63c^S1{zh~fgVeITo?~@0xwiXYeNvP zh@DSQerPfkZJ10ogioa8axbRq$V#3hB)2X4*Hvv$DQo-GDR8ToL`Y31j{uZmPfbMA zDO<_ir_inB9$^)ChAVKt@$BqJST(FPZJ}%BPCY=jaRw#?9IjmBccA|-JE9aGzDlEg zeo%=%7G>$qB1lx89YeshqzNP9V4Y2bdLDuN2?(_%6$Z0L368S~6Kz}SMGE)t@mmsN zc-{tuAZhnI$c}w0ld&HggTlOv_yo8fgAE`4L#E?jYFxlIvpGP*Zau2r$I6qH{1mrxV-_P((Xe*bOifCT2vO#(V)|9y!dZ2Gsh8;} zQ?sCNCg|@t{8YP0s#TOLou-F|(Kd(lAtMK;sg)c|G-j$*YY1YaLz?{q;T^eCN-_4h zpZI%MF30$%+~z2klD@+^+(~()lTnS1pGMpOoL$T$A0;lXrQuTRuP|s*x=rn$Gr+d4 z3I4F^6Pv$E6^GF?I^-}mmKpx1G5H^QdwQkeT=iGlw*C^yf0jDQ|4+64B~zlYKmRHg zT-cxK^Aj}W9vHo6qx+s}7*IilC%txNb}60<7yfKW!hvuUo>Xk8iS*C+N1q)+AdEBb zGcPD8zakoPHhHMzbBa^-*%ZKrA!exlB&)W$Qb;o?vBr*(VoIi(IU?Vbw=Yv;#cPOQ z%cthdrSPCec1md&rBcJ>T@g|k8_wXJF+-=+#!E_c2U*N_@riQy4+jOv&JYZpDO+jR z>-8s_+W~*jf9@2l(rZWOuYM{1)i1jLyi@W2*I=nSn>tC@+nUPQ+grOj{A<&(%G&Zc zf@t4jiMp%LN;QDiHY;r~?G3GK)urL7sz?&KdVU=acE_TLA$-5RJjAAjRnkkD`65Jjn`R{(1?A?_+?MiP!W=HvIoVjJ8mhHson^bb zCK-2PX-u2WWAbJ&rM5S#fQ)S~-jlS{qjGrN45@v`>rzi8rHJsFGAg7zK6s zJ)0yWejy8z^(ZyQphG;H!2|ot-rY1-cm$)Pzap7soaKFpEwxZ@n?mU>ReMCcFW09% z!B%_3Bf>qp<3YOK^-KJ|%Si8yQ@E))xW^eXNcF~EBgVOnA;#$UB}eJCoA6*D%5_XQ z>+qEdvzV!4q}`2d;sbL0k#`i1bu;F@JW9LsThR;uD(?DN40We`e!x;xjrb-w<#Y=`i$V$+fEU#tq#5&}ge#UU~733BA zBe4RaFC;iUfm?X+4MH2F630E>h|()3W;~9yEOt11oZnaGGO`7Vk+ukY~$)| z>1HZsX=5sAY;5Z6ENf_IXm0vnRzFou+5y!R?~iR3g=Lp5@eg7J8=%k@g&+XNQc&8u zk%d+Pd?`43`vkjg*G_DASv=S!l;^-55#~M$!59H(EWjqASvVqeVbqC3 z4oEn&>PBE)gvEYXeiKfyv)NsFtTrn+$}WOWtyW=XglP%{vJ|+#$vjZa z(xTX?W)!-ki-W6D)gW9|-&k0pcFQ%gI?^NbyfunbH6~k}8goibT-n&|sNQ?5Mm8Bt zo{R)>m3dfoZKq6@g$kvaQgW=2E94!aP&SL~@UpN`o#<|AEv&t0jd3!IOe@3ir2$>^ zylt%0(ZApJJ=u(xGV+PF-Lhw};*pc>%*4o+JCh*b&BM@#6rO{Q0u5s#WGWvIm{?#9 zBj!^;W|sdT5YYw9hNROXv(+XxgFr?J#X8ei#w1Fqk z!8f$#-f_zKEx0N?vxS2j;=53N3^zirwR~$OJC<(teCN9|;<`AXI=HE5YNQ~0W+up| zxvZj{PxR)!iWjCW-Ig8CDHCWk#0%vtVOdMULc?IV!z_lSQLov;T*|y!zwPQB+7ttL zU?v!p!|rZS4&oJ%!e$sqYH++a!KbqFQfoCqGnfJx#auV4&&7;mVTJ(c$1?_^{d&lb zOnXQSm!w3~_Zvq|b%v|`bdv6I^wJXtl>K^$k7Q+<^l#p8sBnyYPMe4enXluVhw-AI z@a!F*NYbiI!d7fdbQWxkV&O8?OzJvGZ*oL!SeQj#9jkh;h5W|i-A#MKU%%ddjE0YY z+$YAwCz|J_Q-y|$OY2%&@V~`C7$fcKE zX3DpH%e}R8wDG#uA_= zu81aAn^uMGZ$ZG8>9wq&M)6H!>(a0JHdm;7;hx1KruTKEIM=_Pqz)Mjq*YZ*1&XcG zXZk|?;zjt>5Pt)mL>hIw0@@SV<%J?4qsTo?z;Y88GP>k&u>EBlz-+p0jZ;p{X4eTL zZ@iQiqe(faxGN82c+HgcNa(>8coQ$K&FyFdcY; z1@v~{hAL%lfP)cUAU=>vB_v3vOo0o&vpaH|N+mb#P>)K_4}N8apNaqqvQHe6p|x+6 z;UH6m{|j!0r2^XmrZ#hQvxDO*R|ud-Ps=bT8MJ&~Fg`^t-(|oh!3H!mF-3;}zh%J|M%P)C3KgaUaZE`o>X9 z`0;Lkfee?(9W<68&ayWg+!3NCbBM&(x}XlCUyQ$30J?Vw@EcfqT8q@TIKc31pZEyw z5t#Uh?&10MC7f5`gb32&6P)+b90bWEtRJ5=DmAN?R}T6_%T;bR=@Ie9PC!{3!`x3C zhcViN*pISAoN~mN`itwG67YwNN>Aw`QtfF6xs9$LsuY87YUils%)P>@=kJB06UN~h zYQg|sU2)Q8MHdT7DS1ua8=u3v)w%~=lE%EUy@g$|RU(c}%|vwG!TUn^Pw+AguP2uH z7reYf{BOaF`oDZ9VS76>OLJEzLl;YXyZ-_&$+q&Sf=FY3woX@r`GW$Aib$@Ba|-rZ zpb=G>RN>Gie1z*9(nycvwsqO=l`Tn_?n4O&5KVJ>wF_#thB;W8SswGhu5~^>=H~Q) zPVNBV(isy5?9q5Ja5s(uV>7%QubrL)GeS7gmb@nOFSY`AS85y$y5WWmjuw8*@MADB zwKLDttjRTJkx1gtQM_$&idMmSh7C9p#ilWsp+D6r-RP4WVcj!#jkogPxA{%ag9s zU;N~9qag(;Cpy{u&`}5Vko+R<-p=>zDnTXYac6P~RrsVN!8FO{MaUAeA68NcEpSTeL1$Kf|4njPYra1w zK}@)px4&TjDcg#^_?E|iK{@tc#KZWX5zoK-yAp1yZdtlLuar%sfUt* zhqCn6nvs!IQfY`bL?zE!5XKU{ENTh{M7YefOB|h5ysI4TEpDq>=w}$y5(;YQRgA+d z4hy!^=IB*PVkR@5a^93oem46fjMtbACAu`%sEye02|j5$svK=&hP&uXi}B-r7K#62 z1HkPNhP^yQn?|*Ph1qSR!)#cFhuz3bq^H}3w!@5q-R_qKCTnfTB@}5jkxD6#)iI2n zqzGGRU@OCvIAu6y63J;+o2cd^dLzL3z65(nYQ(}!iz;fl=73^pP}A*Z=PDvaWB)5p zV$^`MQbB$bo8G<^$JD8dEK2&ZDv16h55u+K_hzA2!v&Z4xr6SYjAod&!g?qZbrF%X<1xM+z_%}&Gmutk#z~z^IkX{sN1kC2`b3A%XjhxN8 z1W<8`dV{T~iU&4nczQk=NsLiYyd-$#~1k`dM5hUB8bcxqyn`1D8ekPY^;DXuT& zc-;eB>jc=g8lkbRyoX81YLl|w@ElTEN$b6@0d6HqY>g1Kd<`y%%G$d_;RJHh;C$=M0F6MP|*X$A5Og{hmDTkL3! ziS+E~3#+e4+4(KDo*^%hyCiM=V&Or8`s1%yTWH%qp*vv{k8fe$qt9rKJ`9M^07aJw zFCid(Bzd?h!dA#UH$}aaB`;F7xhg&}4lJ{KAFqmYzO1N;zGvnjUmgqE!kmBO4GJWJ z8A3eg2xT3pxJaWE7vT}x^ir?LaReZXbI(X#mgu56Igh_|NUGM(?>RguMg_M= zq&wtiAUUrBxgp;Tm*uATcQM2@)T%oBy)(1ke%4|NV-R~37t{OeO;H5R>cyN&e{tAau?m{vqLf=6gO)qzMbao!*zz8u0GdmVaclVyl``xLJ6Lh?F8&(?bYyGeKG zu)chV-+i~zH(8FoyR9s1tjZXQhcl+Ld^DtRxfNe`0pHcY>A1K!PHbDTtF6wtd<2Qj zHn&jWItWTh95200}C(M$vaUP;{gsSd3{KTE|lg74u6XDqmhtD?5WG;^zM}T>FUFq8f zK|}@z8?P);NK1$%*1Ln@KoAE}QKC3PT!Yf3ch=xK&BB32vbfzaL89&=l!@L=UMoQ0x+Qq*4#eM(Y$($Xs&| zJ&|dUys`?Gx$8p227PcDn(sU$`H7!l7QSKY%pG9Rri=CT0nN@1X>x6R4#+&fZ>m7E z@B1l;asBE2w1qSweR9MfuxHzNxkKnuH^o!HTE+CnPqQCqF+bAX%{8<`)uHuBC3b?R z{MPaE5ch?)N_R=}+QhY%r9J3+(ihjsE-YPE~t1##KlDUR_1^Oy-PoUT+OHqKu{8z>ri1 zNTS}Yh}72qrk306u(l?(r@rm#t{x6^LIu3~f`O!bKwxT74YvUM{fY6?6Kj=`&5lDTaqGgc z|A6i4W+8m6^lHnyHy88X0i@W-y3D!v*RG-3OLqLSaqLD1cb!>wtsrVE;QF0G5gBuA zxr&)>Gi8L;)*m%Vr~|%;ZY=uKnNQF#d8Bk2T|8;{vMY_^upaRnf# zcne261NoM;gJGE^m+UP$Ad^0UEpv@FNU~2i0x#b^kR|U@ai?QLTy5z9j(4D|>_V$o z&AYR}M^-n}6TIc=+6V40(d}GSaUkxt>axcdZvF;08hT)YfF%_6-|6dV9$R~C=-sN` zQf>}T$_9|G(Pf7y-vx3f>fu)&JACoq&;PMB^E;aGj6WeU=I!+sbH5H_I%oD1hAZtV zB^Q&T@ti5`bhx+(5W$&%+$E{Z>30UCR>QLE-kMh2$S`cI(s^3>8t@vw1lfs?_oAf3O0(TGXet6fGa!H4Cc0s#(f9x|s4qp|pucb69f&W{y7k z+~uCM?-px0{PKXSp;m_Pi=IQ=4SEX1)RS_Oyox-^g z4c|8VNmbQ{0K++9fC>i&QdUrPIWi^8_QZu%rTT_|lUW{fz7#AqyR5Gv&__0p@E7m^QMN1FZE_Y7nu!ZN6Jm^H$uPK_~BC*L{YcQ{6g{KXaVmC zF!l$ZIUUUIf^<8ha69u-l7Ch(0fjtWtUXwj0H?duK4>8xWExTEY9zG8GfabA2v#*y z7wWzW-i5hlr+19k`6)f#hyl;*iYl*U^-D8Ze$!ZHhUi&5BZ%?(Y6MUU#rD1pKGE^h zUnnQOG_s*FMi?EBKpGFaKd{(2HnXx*;dYs?rEV?dhE>{aR5m{vE%{5}R#b`Rq> zzt6hx9+5sc@S^oHMp3H?3SzqBh0up?2+L*W=nJ#bN)K6&MV?Wtn1yFbC&B9{`(t`zcppF`I3T;#g^jbHDih*k;w(q;VO^=lfzo;gHu7oqr@Lfj!f z3cx!&{`j|#8e`$9tv+azfBr2m%(>gPgZnp6enkZYMD(98R!KW&7egDHe?@z8HDP_w zj#~vNyEisyhiH%nC#^+DJi|F~kl-Z~){zqK7>O=S+>>IiNN;A7L~6C7rB?bBv=`KB z;*IE36(#2Z>sG#PFNLkGtt)EQ_LtYay{|93TOZV~{$_3**(OMb4EKskf5xo=Hs84Fmn%&S3q-yvIk3`E;w`Wci6o0UQ#7o$_MYj zSwlylI+LcrRYy+mH3?-(SyhfYGi)#ncaK7$m=iH0z*%$BCH|H9=@ZVK5#DJrx%dS} zbqX`9>s%IpxWbmzg@DqnMDls$jB5`4zxe; z8_2TWIB!m9N+ba}aPx9@DWge|RH5!v+o%P0nYgEVn)8%Vdf5BbZ&vR;TD$yo{GD0{ z))_(YvDO#t9QIu;g_W*Lqh%}E9Bj4roi4&VWvw!yGwGMzPgxNJmo=8HC}uUz;7f16 zJ!mb@nXID;Bn2O=Gkp?0%*zuEvKH{zeC>icS%yWIE83m}S%MIX9BzjhXS!s>rL7u5JC_n~)6lI9rOR~Gm}U~M zJo_G}F|vasg=bd9ZL*|55$g)o%v-9DgOWrB74Ly*sA{995n4IQsl3JQJUWfuT2?fZ zLR{oIEJrZ3UfBI{+>WA^3Ip^u0-<=2QCiOG$+I}(2a+h5B_paPcDPKzW|Iv|_c3l6 zxJ`_mW}3Ku7%34FqX8kyO~Bc8>pJ2t^I!Mupdf{n+xD^&`sSeG%WELyUR627_-v!H1>3O7b%S%w09JfbFXxeaQ{1cUU< zy}>Yq1IKG!GEtHSPhL}#XtQQ*7*%nn=?Z!mN(tx8rJa=T6w6hZgnq)!buxxCrJ-;k zWdYS>7%S}Yd1GHY5j?QBhzcStQiUTXpND*(EU5J!a2Dgve{r->K_Hw`sevqCGv&1+ zW5;H^URKar-eQA`7DK7+qN$0*P7+qK6cSy^s3=)>bq)G(I7N67WCRU5pVzd*b~hvh z5J2x<3^{bxF{WBWeixgTdNTDj+`^W&PDsWv6-h$FOPm2l;lw7nbp9RMIDe6-)=7g-M>lqJw`(zxpd)NH@he;;;wxTseZo$yE3{Vi3L#KE7waR48B=kX zESjro$+lBC_xfEk*saIn)&4+R^_zDu>iT_HY6i4M^2}H8nBgJ4 zK(sCi>TI>uRkcDH?Yn8x`<)%k?ItA00UX&&@L)@|FSx(xLH%7W_4QtNoc_i%c+kE2 zlkK}}^7YOy_4e3a!a0BPH5vu6;*;nL4)^E$VQgiFsaUMdpjp?Ik2WP;yW0FoI@zi9 zK}X`Uk)yP*pw+pV%#yKhM%sWMZaSV?En69f{!ElLzQnJrg=k;y#d5mo*~@CNOr~Lf z-;d)nwfAhFA8;=TlY56>GCXnskt}x<+C#0UWXXbup-xyZ zArLX^SBq1vaU#4`=UJ%|H#H-|=MQzO zZfN5cu5PjHRzHr#!DHhqeIf|e-=I_T(Z&c*{H|7oGn?rX=Re4Nt9XA1D8EAqls+sy zutVi9WC#8F(Tyz)SvYWtZ8J|<}mH^+{GD@r35ZEx&N$!%M>a-=!qew0J%v9h7pRK_;4mZJB0UB2Khq9Al^@XZX$@wc;ZjAE;os&`=<29G3brICGCR>iWoNL^O z@Gry)9Y8f+4+*RF78d&c42!Y93@X523z)4e z3v))!8?NEap1^>c`%LRX%uXxptukN)eZ%U`o|sa0!et&N^(DmJLBUeA*V9`EiB;Y- z*h#(zBS4n*IcR~|TW0Dc$q?jaUU?5Ws`*^c`${TWCe!Tta5lPV>AK-TF*G*gF`B2W z#^>et8ddT(*4Zt6sqvDIg&d&sr!XhSF4)0}i|B{vrd>Nv11`42yT?@XNjN5cl`&iD zL8E%@Hz|&ecWs&L1fu2O36c-V$*s&9Zbp80y_oPOHNi!eA7q;lQiHxN1k;hc!We*- zU~|vPIi81cbsf`?s7s60TY9hGbM{>=s}rfSfLMH-6x%H4PI0nqBv7pr1rda?%yGV_ zVrs|)$vu0~5(raaI;Lc)T{uA-oJtq)8)`GJB?!9{CX2gHj+SI&wCR1AI7{74Y&U|* zdpM<%y6YI2h8xMjp`V&mAE?JH?aaLvt)vtdKFKCN{U*oDzP>C-H5NLlkS3o<-{8TW zAi!NLrC!P`H%UUr&fx+ktJJ2iWN$b7bDGG~FgOc5b5B4fhlV4}>vY=jpr9a#)qBY! zha@Na@~pAw*ndf<*uc65He_!ar2~nir0eCR%WKFg76V{r0b-#yd(t|eOT;x}H$%@@ z=sbTAb?0tx{7K9a*Hu$F(fYF?x&rmUvP$;uCrxm&PYnJ^VuksthAsw*m^ zZd9GXHw)(2BlcB@%X&*bC+V6pZrVfc=Qi#+MT_^HD?Y&EK1ZGZ2l#O?ngtCWN2VSD z(KBN#Lp`UAl;^SGL#jG{8FaV}LcXv!&inlAh*WIZB6fly!Au!SPp%|~amjX}Wcz%r z$V>M4@JqHts(F8;4#AUOUS9w~;t3SE#7}2cQ2|+ zsanLZqu@TltW7n7C-6ranktBjiu^J@@sar0gl0JIv|uN4liDI|75E9vb*DPl4%1^D zQT-AI!6F~->^>Q9LGmBcXYA{1!L7$GJUh@cW}`OiOjuOKSuX>eps5RGWO@2(LZ8%-g14X zPa5=q`gOf3hpg@So}2MCU`=B$JBQYk*lYJ!gyNJ zx$R}8uaME2mp8Y8r(R^UzqAt|V_?UO66SYBg`|)$C;kO=EWdMCa=@Wcc{AZEN zY7NKy7b6M@L^VMHB=LyIrs!S?D5Eto`8jdTU65EvpD5x`P4&R@mdE2kXB5Js`+k`Y zsDMy>8So>V7?>5^af7v=^op_z#Sq65q@|y>VdbkPwe_P)8v$`a_aT-TO`_CGd3d!L zf_Glg1+Nt7crs`K%{&E>GfIIhFn@PNo|kjLZqiE22n58Ief&=nPmRtrgoUGmSFj0F z)N=1U5&1f~@JfN&rRIhJ2iqF2#EU5!$cnO6ZSo3z2TVE$A`Ck^os#t;^_Dizg~pCn zy8f!x8O*0B>el!8C6u2_O1H>b>}bu-w&gnTVQcf8oJQ0nOc5HqutoXdST;Zp_HD)k z;ryu(M1K5cd9f8elWNUO)n=r8rl)wGsGp}B_VQbfN!80lc)tM8sJ!H>7Z8?Q4L)gL zuNxm0Oa!fTs^aOMd{Yn6Nbs+TYN{#y6|0y}&r4ChC2A19@(Yu^n_WDF5`OJY;~dSl zLG6OITL;-Z6)Al|4d2vYeZjM#8ks;0;G4JY!7kLQ16|^ce%uaz(_%YtZ%t>WYaO!Ak!jJa*!&ZT_IRLUvky(fW&$dEm+B<2}`V*~!rvlT?set%f`@`~5 z?H9Tv6lN=4fhEG0tq1;TkKQ)Odg?Lr9#c{$9EM&{y6}82)cq%tQv`4R4+O^nH)!b*;7C7Q6mvwx#hT%VXQUp)7$0l29x&S1ep-S0Ih#jkn%g4c zS@>O(N$T3U_!*B)|JQohOStBoKU783Y56?vlQQn6=$YqGm|LEXSt-Y??HkH^zM985 za@UpP;zwm~XA$GF{6P;SV9$HrnGx43ls&$9V2&vZqD27H6ph{(0}pTtZ*;0FHnPujOXOv=!n6QgXtQ3~{*ZN4B!Z-QJ`HDzFBk-*#B}qS z)*L_EY#MpHkEQNi(S0((2KNMRlm1JWgcb7hjg%*w!(*o~VmEGw_^V>0g%TzHqWRK% zqaWwE!Dx`f-CJR?@bl=PDL;Ubo}|>7&v1#P_w%@a9O3Vm2TeADj@e_Db(bvJ_k(|p zAqW=ZyKor@zG=R&1n796=5hR#;)q=**&96DVukjCEPUrZ(}1R%D|}60+Jh|J3tlAz z$o&D5^8aD?MQY(2!hK07cuuN<$l#l>%lQ&i zHDHHwQH&_K0*d_-Fhoe~P0`+F_$j}?|7%ryo)U>F^GZ~9K}j)GtH?I<)hIl#w!xVwTDcg8qrc#Xy~0a9!1NpSczciN!rwFys7Mo8x?mMpdl&`q(%0KQ)97x4 zXrLtX$K-UWCL;OsX|CWVVm*S3fH(C4#>V2iP-)m4HOG);Ifv?r!7>cy%X*UnMkHm1 zwYxpwP5*pviC8JPe0nl{_?MiPD+Omsps@`C&QQi<}|JWz9gGp2KIBqX#x#-xy8LX)w|%t#>`hkb945` z`R$Oq^BvdhuZvk;cXq0z8=o&`nylkfR+!yE=K~GxV$MtCL9}ji}J3mD$U>$0j zP8a_CTS55FfK24@-@233zprinHwEEB_VzB$E`JNFWDPCtlwAy+T>fX#iKh0J8WP`N z6L=NMfDIFv0|;97h@7$%ZUHNFXaiP~K^k{SbOVE!NLmFg>RB4S0BZgnQX91kmq?wOf9&a>0K#$WGq_6)#1frO@Sj_P6zW@J4KhH7FoCnnoN zJu!b142F_nkWAQ98V5sPUcCEB;m;bWNa>7Z#mLqutEM&v%7c*45)K^kZw({iW6y62 zqvCHGgOtw-?@rocm`Nx~AU?`jg&RvCyoGmRK#rp_Ou(^BGX^xB)9lTw%eJ{>-x--I z&+sdYZ+%2)*Sd5xM0hNB^cJm0=r^z;cksnvSchAC*%1bO=-6ApxEtZ^TDNoOzy_-esc-&n1Vz z*jmtBjO*fVvSET^ zGNHe*kaJa;x}b#AR`troEgU{Xbg}(#`{QUFYau%BdN+bBIb>>->+C>?la_i6tiAJjH5XBLc)Kzz_ zB~xndPLF5rr1%TDrUi6DGUEWuw_;Hf{eV)M8{l3q(K_b29+mTckTnacJ^l#@%!<|K3(kS zWlQuT?fex!ci3GJhU;1J!YLHbynOK?jsZ~pl1w}*anoV=9}1qxlbOOqJEiec1oV5ayrkRttwqs0)8{bzlO%h8Z>aM^p_EJ`2X{2wU( zgDf&1X)~AzS_tK1(5M9txh=PYjCDqEJ5Mw7!h}G*2-BXJQot1Yp-jJi?2&yS2VD&b z$1FyD;0cFxM6%Lq42+LiYu{uALU$P4)Zd7SSB^YmxZ` z-55W8I;sV_!N9_xmh1qKdju~XC;7^`WetPD+=IqF95XNeW>2`+WPa_D*M{>4)E)6@ zMdIyhN~Pt9+y(8q9d5rP{xg9uvD!|y^tS|$6blFl@SpPx|5ait>S1c^`rmKNQq?^T z@Kmw?$Tm&bu`h+#CACpe(URLP&WKL!q>)N0GkwVdu-|tXhQvYNGJFUVu7{YXAQ)-( zAWc000pZ6yltW`*9%KRHBT-`^U#NmPaq>~Q@l#jI%pWd5`N)KEZ}%a0c!{|mCNG)- z{FuWVoLB?N4_`h&`cV7Pz&=y~43KxJKz-Cx^6&SpL|q}*mk(cIaPq2$*>7nQ?`?#8 z&_$Sg=;V8_haYc&881Ubej$XA_o$z&0r^xFdyBaE*f-ZW_~-a|>wMhX?cNq14i)Ae zCNhE*x6HQntBK1>sQ8LgG9?u3R2qx6C5vfkO>PzwF?9x}c>#5^7V+Xj-zN&ESLv%J>sE-m^$A9Q<#yNgMKhxkHK_;|n%gOQUK!)(9J{7+kX*KG$&7Cn-fVDI0Zl7KxMQjm=2gF3f~3+z}0&X$>PTbgdgG1j(7? zpj3js^Z`FbZ*4_7H}+@{4iqwU&AZO~V)ES-9W$4u!0H_x;p(#4TrOu*-b<2T;TdBg zF#akdz)5`EJCE)yw|3AiVzDJpAMkob%a#5O z1Rn9QLDU5W$XceAW^khRS+C<}`E2x_P<&L0ZriP&nPWd&&yB^n`LY^uni&OMc7 z6wf|T2>AW1kUvYqL=5_w+C!@{zxXMnv|7KFfZ8pc&A``1j+VSkLr0QH+qGtjg>k)9 z_Q7^9!2(Y1IA5NLDpFDwfq;|fAVO`ynI{C^dL;UbuvjcQYcR%Py$xIWsWa)WGtr=D zjh)bTyUXaM$}XRau^=+VIVwlHrlg}!e2VP!@3XTToumQIszp>TD^FhgaR zhV1xmy@^D{8=Kz{x2}T+XL1vYvR7RLdP^63C}v3b>wJd8QkIJ{r(J>!wwlJ?+@huV z4DC1$Ui!`1n7t}*>|W&HUb7XZCLguikty|PgY-zLM`Kj_eknD=z7#qY7WH?4fRg66 za=osWmij#7jjGOtva7jm<@B zQv#&XT@bJgyF2IcteJf}{RR}X^Hz~bK`W^z2QG=eF; zl8L+m6mDKi3}tU1@SbY&ysq4reWH&=l{aaPJ9V!tv$s>#9}sA`a;ADc=AL(zF?gYq_6S!t5yVrIp#$q;{4!}2c|hKh?yxgp+%w2 z4YfxwHEssjXNLNZrs1Ay%(DDoafzGCQC>H`Ovtn_R5c)>~JY<~3qN%EfD#g{JEs9}r^IC1`teKotg!XjewNAR_0gfhZOfXc@ zbY&MP@kSRVE)7FS=)x6IEqP)#F>qWd?W`?*kz5lYJNTkaHEG++3(+4Yiu^EWnmHFV ztsPd?HmoVRtSNb{4UOESFsgG$lygVKvK?ca+g3HLo7S=r3k{3s!blGX7DybHKg<>$ z*1ueg;co`{G)_Sp|JI<}1;k&jaN@Ue1}h4nQXbIOE0G}$0 zQI_ficsmj|owWh;2G4ItA9ui|D-#F`p(wMbG_zMk@g>7iH=2XkQ=R%?JEc^Nddj`v zKx=jEObay#v$55#{35Anabcss2WweqEsA;Pi>0v$ zm7E;2&-zf4dv)`MM_LyyeAcw#3@UZz%+>7n!!VydoW|C2RWn3@S3GtrJBz4Qauw;I z?u}yR5}jk-IQ|7MwTCxr29k>kohuEmX#;0_hy-oxR{3ai@yUAulHQddjFF4BAd0;6 zRa;1BD))j~b(X=PsV!7or64}aJ=#i-8IlU7+$9LU zqNZpVv7s_%4|;$BI>f$Q?IhYeIV*5Z-s-_s*QDz{-IXQKcfI}H6sQkvI#5~rJt&uY zAHuWWRW+Y!z5R%P^Ulnr@9{=GchIzbVC|S2Etw=Hoetf~y$Q+wdsFKo^CkEd(`1ir z_(3b}&b1RH#VLcK8%a;}3EkU`k5tKMPA_=v!6w0MPeQ?m3yAFhVeFmaEAO^#?Nn@4 zY*cJJ729^jw(ZQ=wrx8VqhfQ$wkoRN%e&Uv=e%p}eZJqmn0NDHqL1-!y^S`W{{G6b z%U!ohHzZIbYH-C_JQI4xM}{$K0l$slS|vIsTT@h>q;e`@Nk@JnCZ89R@~x4>QO$6? zYc<&euAI43u})(Zo!$C=@lQ-%*CxljC%8#9OXa1AXz+8ljhN<4Yes`WXJC?stR`_+ zI>APNv-) zR}@DB${lS4{T)hfZQfFq6Q*b&2@Gx_ZpuHpz86^&l_(B5&oscMD+}Y~`b2HxLUA|6 zuyiGSUZOsclTU6JEsK+4HA40rjY7`N^J?;>o9Efg&4n9CC-kESY4W1WKjZh@&r#M2Sin5_l)gmV1pX3L(aXJJKM!#ZX%dYoO+Wl1e zxX=lQjHn4lMpV4Rp$Brv~y=D8Bi|O3P4sd-p=>2}4jI^qF<8CQl>wfQ{2>)5T3-y$*<6E>l@)RDC zyK4sPTT_7a6S-{7Bd@u;a?jq+ZX{r!)3bvI@$vlZ?0l65`Ix&TcV>Wzk01528Flt) z6eA#koh7H~zKtz!LPm; zlL+JEy&)0owze*4wp=Z~$NGz7_(uSlOX#g^OYvDa%5CK}Cx(LVROjztf$|^}wgH|3 zrl8W|J($E$wFL>OF#iNb*-AdCjeZBdc-E(SZtZCaS{z%Jk>UHNI#$=*Xkjr?6c*pW zsBe8H?cm*|i78Ai45ZYNg6pi<9+Zb|=q9hcB5RI-#^W%(oCyPIOs zu9xz2dZ#E?jNyrRl=5>?J;mb&BuVu{A#OSB_#_k5pTlr|_UtLnUL)mUOg3^M{JdFb zU;)W4jfG5J6kwIyhIrBH`+3Vp!;bNlvMo`!9lWf9dgJ)|8+H9}P~2YfBXn;nVg|cU zMl#yZ*^=0psvUFaEc)LP*u@T-qOvO8`vvVU!Bi!&Bw3Qfu&O0@v0l=8ccW~xZ*Gzf z{3R>!B}I(}prXQ1@LQS9+5cG6aV+R^%HB?F@iP>(I|^MiPugFOCv?HB(?VFbK`vWj z_0i$j4$I=i?2xM!!s&iP_>5tXji^&Gw$mQzT1e$R5p1#rg{SQ|%fT;pfm*n3GQ4 zwmY@uj2Z4nEKS+Y<5Lje`>s6fd({rZ6HTJ!q0q%#Vj=LQ4e)d43g?q7VkxnUh){ZC zjev2fa?OD7G3*DP;@MWKymX)ug*mlX2js<$O@Cpu@^^An8n|=Fyx(PM1hUK4%eRVY zCrTPcp|cU+ypM;_3sghhs#aM@M&e@U>PfdoqYKgMSD2JSO}bEKn*Ay;?o>eGmqiN` zlBJ9)yH;jX3|`j|t1)Q%$_6^L`b`LZC_&DsJxxAZT_l`bN;IA17hAmqIGSR9xKzCc ziZrVtS;a{c*CovxUm^pPk^>F5sWDc{?yCBA3k$)Jm3%kR)m*I%c=y-W%-4vQ% zd~}??(MQDKn|E=JX;|1}W*}HhtPYP~NJD9*FVX_kX2HaWi7UbARk3-PaBN|%-ol=j z8}%%?$3SQryUrTX;4oF4*J$to>u;eThO&*oYcj+OM|b;wwH5Q5F@%;SEmBwN<7jAo_IdjUlWL89w1T$>vB*S z)v7T85qag!RDHGm4Oi4=h(o&?hLwZoqj{&hIzs45*qfM;lL{gR;U0j_y#g$E?$oAr7%#NV*3%zENQx4k-eAHykzLpb7QcRXYsnKdki!A|-~|q+ zS^rjf6Y65Ycf5FId?qR!*!Y;c#<6#s@&vl3A0m`H4Ci0!zk#S3fVF(NCJy_|VT<%+ zbV5+>`chieI{GnM{pf$oukxXy3ie*I?~aLM+;2lbW0eu$)i1<5)G`NC-}bD@2m-+u zf6@+y284?mIskSfV7$Ch;W}_A>gzHi?XJ*Z0ptoRyKpaa3XnlPf#TbQT3D2)__q)X zo2(J@Gp4;{s5;brLCTb*CLYp)bpmtrurD}s&`oG^1qGro)WH~X`3aPf^BM_as&N#H zbnkgTEl>s9HP@7y=rvfwBefRt))+%fg!>ApXpe9-n8K64LdzN~D$INjSp3@N4$HRR zOdj3Ll5!>He}=>DNoP}CJaDQQ0!b@QNjA;I;y2RRtlOgO>>;OzG0 z>$XjhCg#$SHV1_@X?CE*56PWlznM)TX=PbB1D9haDYfPT1->3uP9Zo4cVS$&ru1Y9 zT__0W*@FH~%nPd2Q82V4-n#V!7Y*+6s6%+VMz zRx|tT#!m5*yYaSi&7t(6&` z@QbhROI+&dOE5YvODU>yTRNAP4S~%5di{{l7s6yO>D)mw1(hCtNTyxtV{yQUqqv?d z$vYk1So@#ebe$dilgJp?ZvGvRYjfsX^Vi@~);`>LWUh=ZZmw)fiMr7NQ>?CTwVA^! zq)bZ}2a4+Rs~8@k9f3VgUgwS7UB`S!qdsIUGktSoHV+JS*<)LiSHOo_qiM*Oudmbv zhh(&0RAq{iWrlD{oJf6eOHym~7g`x@+*k}A88wTe5t3#kr0q&C8l;+cA>4^~XkdI$ z5;c$;(+J$_@e99Q+Fxv%mD0bhAX7>iZ2`-i6OuFEEb!v^b49LX_Os8MD2YRgWj@m3 zH4J{>jsg3#=^rQQALpp<<1JvwWb(dq#M(~mDxEr_bXlUF760c6+3FOEd)_B;py~5Y z*Z&I+_0Q<}e^J-6)verc7tw*sIGPc>l6YUfD29SF649(k!NYu$6Z*>IFUUkJw>vDW zJv>Jg%aWrgPD+uFl-JcyIs;mq=0=EYE{&^I#aV<9>snp2=zA{i3*nb%LKtm4-mpvl zTZ{j3ljSI<@rvsY|NZobwQU+$k@yDfW4BzCs1Y?t6)uhviI1-vXwI>$cfWi#vM@ zC1L{bMg)pnf|7v5qhK|^4Qf|gg=2FJlNqWPfK4QjeZ2k^A2yaEm02e(*tBp>i@{Sd zQqc`xW#$El*Vw~s#C51(;W%;sfNP`_>Mr)napsy9TRl0WO6d#iOWq!1pbc6iIotB* zee$VjomMe3S{1K`%K9EAzXnG2HwC$f4MP`d9Re)oKdzoL9PO~nU+*Lbcnm!Qo*hS6 zorbfd;>{p2$oM!j@xXwfz{cuae58+Y0+<@N<&x>)zA;p5gRir0o|+gHZOu2k)@ zZ`2ebG0dv_P~tNfwe}}R2d}C&oM)Y!JaOsG-oSPJ^8DQT3{T?=t z;$5^S|KtQtc$S9p-Q@hpfKh*~gh5UMmwe%O%sdc#Ld;%mgn|>Z?}zg%`cZm2*p#qZ zK2giJUhb{pozf?nk)tP}k*&c4f7%WsDuP7WXf_p%Mq?BhN8ev~7HBm+_IQDlo+Ue( zVEZ}!DJ4*%^K?Dtb|DE3BdJHSeznAPpt~ZR1kB`yv(3^y?aS9A=~$$hY>~WX9M?sY zI=3)u#-FB}vPMK5m$x{b= z0>@f`P1ln+C@b8CD^MQ&_ps>0!w#!N1ohd#DA*cGN%4XUHxE*dYe8z=AfNFM0Fcq+ zCcnopA5dR?THKe&zq#OUL7$Pg1XB=v$gOy-xAhoDbas)Y(&9eoqPT@%iXB!}RD7Co=qr9Pt^-i|J>I-keB#k2@uim?oTGp`j=ttG?*r&lq*Lf>tL&M)k2)kZw*5)}{a^yN#EWt@mR z#&T@d%T=lBPu64FJ;?Ckk0nhtll;s~&@#G!LU(2?0M45lKC-F0?t5D=ZraakEwU!| zNHnJ|-*5TZHFZK2+!2dO-4Y4H+M@;V?M`XkP@`F2jVC2<4~5kpc&k4GvY$9ycWCY_ zIU!Y`wvenGQakX2EI}X3_D0JRR|@s|;ykl?zm}Zu)#iOY2TGOzIGy+|4H=>s#?m{P zpk>>X4iuGScL;n{IjdZE^b9Qwy8H}~0LTSLs%^19*gO%ju)I5SeIFGI6KGp(Yxz1AWu&5JUGceYyacUvL(?c zo8$`!h#D9O2@}Mh4a*7N3z23qzOx3)o3k(w4^kqytWw0vDYt9hzI# zw3|G_tj^YUwWS47!HJtfFbKUVWfF+xI#v-9Wg|bN`V_A7zxNWV^0ENt%8qEBvSAyIRmo-CI*!OCQPb?IMSb?&sGyO( zzBOViJ4a^6NxvM#r&|k;^0Sz|lE(K#dA`}yC-RyUu^jdwRH?X)4ema@zmc3Bv%ZVl zUTSFhM$4)~{T;zew)`gyBx=9d66#p~%&+~u0;?!g44c}ihh|Ger{v<`Z6ev?8nVD* z4`a8A=3jKEzS=AC&mUx+IZ7^fhnEq&Bid}(6h9jCZO6{OWg)M!w}FWALL=+*_2QX+ z9;p7V7j$>?i#;FKk`!4B|IX3bko*-^wei<2D|^*l?#|73WdU3c<0un8;U^tD5sSz#4b5L|t ziV7%uxcK^1gzKn#sH^oXf41YV=`F1#;`YPSi#b7q( zD{2Smzk7TMMpC%g&>$evNFX4@|8ph$I|VaDJ=_n?4BOYVv6F=do(lt2gEFoJ!TOQ} zHlb;?mlw#go)z3RS$ z%y0oL#E5EEFBmm{FjC|pso``GH9^0)iMPz~h$`#eSL%#wNpz$=Wy9xrSOUdQw@r;T zSNX=nTW|>ThHRD>r{H1)&0BLw{kkoxmij3pV)DroWOG`iGtjQg9dt|OhAvB`PFbdh zE-DK(K^Znjz|Qeg_)Zs(U79U87@4L-~C zn99t{Pk1FR0*Mq%rC7O)%DT3B2r|s%VKvQ*T!*Fjw_0h3| z{)RSQ!pxwD8s~(@VQ`PW1avInV(bZ+CQt@xP?yK3q@7Nu*=D#7-__Z{YIvf}>sypa z?cSc2)3Q{D>9;5GYBV56w3&<%$xlYB6{!2wD$Ka#g+`W+Y?Ql%nX4(Yv=Q0gcvsCB zlU2o~SdR#j<5}ZHcP;hIeVZ^i1^tZ))Kn5HsC1BKIG4TmDphEf!#G&u#s~~Dn)1cg z1Nm3OYt#3KaPMLa zkV>Obk0)NOeQo9Z&vCAg~!MIU@rB zWLfi!(J$Rar>7vj`k_Vv`yV;?)O6=qMxJ+7;=?ITnw*gHN@p3v^mA=vFvqt}8l z8k9HURMOgY5b(4xluq4gCwEksN5C6$&jGY|XJKHp3tgy)(^F4+$6y;Cq(ZDwl!xCuFm7S# z*H5>VK5&;t!BthoVa_U;RkYcc7f>28*7fj_M37>ghb$?b^n2QxxYJu9K*#Uaq_mUf zUQeUGR_aWho_6QXF2NK^$$W4z6{_)x!Ro&s9p%6yD<{(1m8%hCFJH7tRHd_8O7NXu zU=X^9HMS6Jz?;oZwe4q4Gz}V(_(S&CQp%gsjg)n3>cvGFPBmaU6BxK3u)_{pE5s(#Lv))2V%V z+Slh1wdgXZ@!I7vM^xBtOY?~eHtVJe*yjosXwBj9Xc}Ax5p6z#Bi4k7-ahGF)D>zsB1iH}3)=Bc>yEMzkFAB6a(c?d@n+ zyj*sqNOPLZE7b<|b%V}Y&Z%`}YeBoW0<`xiqJLL%Hj zKN)^z7JoMbbXP-C*Z8kjw+O=^`~LmHMTy@DEAVE`a>;<1(2Sf=)IuTcrpk8`my3|FPO z!r<;%ok%PZ$Ooa<{J&Jcs9_&gnxxgH=s)bx@e9YqA>zBk5E@tc=3K~5kc{e7Lt|s`OB747iePjJwVdUVhaj+F=t;Zsk@f4=?#*Z&iVPv`beRwLa%NcHxg zSR8u$|HE=uo|=@Wnv_(Pkdz&t7^fYZnBG%Dq>@#=mZw)_WL98gY-VO^WoA>hcSS(_ z0*jU5h>mt(R!p9XwqEiNkpC(9k+CCs@?o;^VaeLRvHY(-dEb_YLDbWq9|Y%9_I{pc zf*873SR2zhni!c_*gOC2Q?SK$+72+ni@Lo_p#*q7#S2QefQqJI=)&<~i3gBjCs^O# zow35SdX0`tudz+McZo@hmS#bp<9mllG^e+j2XyUGA{U>Ud;q)x#+d*Qm(9R*!WdHS z5Iw5W7u#!F5wvV9ZXRmVm~YPzHSI0NBo^|xX39*yXL>)$G1V4WQ#+>T}5)QnR|X}UK! z+T`-OYIi!^1b+APdxx|SBL#ywKVD%&?u+??Kb`z2^Na07?htpkb({;z4CR))7 zG{#w0Iv=oGO}GdF5|Lzha}6zFfi;qIR`iQ}w4>3FbWGcU23C5#6Mb7yOlaN5Ny*q% zR3T?v0WFjk#*BJC^&USudN^k4N9-$4xO2!t18dIpE!YcwK{*prSMSwDSYmYu$&|r~ z%@e|A{&ZC(Y*hbk^J7u6zt;vZ;j)}80`o^QjZ+) z0z$`ID8$l}`D~J%IGSSYYHc8Y1m)1&%%h?7acG*zN4{u?Mw|nsB{FCWr>Yfm3jT)h32Nx*2 z`-dh~PQ}A;vQr#kjeO4-{$BD#v2PX3JJcxP3CO8W9a7V8{X1pruTo_GVG>*NS%Sx( zum1??{#ChuD?tSV$4`#^fBCW@QG$O>!w~&2Z`OiyJ?IFt5}sB-0~hW4I_O$PX8|ht z+n%1+KNMA2r^BBA?mMCB=GmJ&=qPe1w6I9woP?f-Kgxkl7!gspyd+6!DvA~p>!u1_wjqD7AsTHHPINJbF|bJJ>^Om>dJCq9W6lGF{~E8Zy} zE&7mNDd!q8?_3vHlXqx#uh`@%`om8k)A{W=}kYJIe3xw28?w|(& zXrLZT``$6)fX-?|}q7+!|Ti@pd`@V{0YzPf`Z#gcNf@YZn1$|A*zb zV6r7T2Q2DY=B-7!b~mJX93qo&^2E*pp=L9uOhp|tkb%1%z$UPCpHA#}GO8;Xi#%qp zKhIXf>mkN>IxdpgbI?@lL3n^j>6X1#a0mtg4r{(H3>Rl=rwc$9B`#R?{QeMTP?3tk zGV!n}0FZffWt1T>;`A*v0ywn^S8!bGDyJHlHt;b-oi-cRmcXSF11GU9Ui^oM)h#sS zg1$iza}jf6lU(py5POo}o`d9j?@;vrDFTe*8559CyJ6{HP6qB z6VPAavfGb=P>>}TA&+4)68PIe!VHt8IYzYzf9E*BvJ=>g#+z?L%fsO16Httqes7ge zzC4FBJg*F$_ZB8h1(h`*@!udGuiL5vt9xrP*5goJ*{B=W+bed4NYoS6oMsVc1H%?E z=Oi;ndHzac0Dg<9)-O88axX&t@V7|*U#q>VN|yOA>T}TNgNN^bvjYBE`pTd7l&#t4 z`mi_n#6bVoESPMS=}!tY+Pi6oiGfZ2ZJ~a1pjN(uF%{8g#H1)3rXJ-heE4R`MG3s7 z>)2(=Q*G~9CY09=XgK+BqhHd^q-(X1l_jV1X69p$$JM&s=KaVt!xjkI%|tKqAp(}= zY<-^5tUrLPIgL9-HN#qQBqBx?5I}b_s-H=mlKWkM=9ewd5UX5b#B-6iMr#vSv6+fl z%fYIjA2~Qz z1lTf>K_}Z!09RU*(T$N~=h42IECugLx1l)S?tLJU1v`%+H(*UF4UB)*<=z7Ve-cU*sd0_d%}MD+DKxGnLRinyhmeu;@^#qQe+)XK2PEc=!pEfwk_4 z(`WDmFvl@{$?jw36ABXB#o*IK(1DTeG+0YFw$MWU(FXn@gE#_R4MshxED@h;4rY(L zr{E-dD-!yhSj<7c)c*70z?Y5(6fJA7n=4>P3SSUYem3cp_NvoC4slI$kC4|mJqiP| zXWpWPcka7zuQ=1hNZi3*+QHY+J4v)>G&K+MZ%s?KI4DY+-%5lMc-n*sC>$$Cx9Mlc zNkYB$Ez0ppa-ze27Rf|eJLX^GzmUAqGp?LI|7Nk#FV#$-lnb3qNXk@WWMfm@k!|2j zNc^3`0)%vi9WK|8xn<%-ylG5>vmr1tWv2a#pvM0JrgRuHSIU+FXJoaUy>Aqjf6t- z?qbzZ&V46;j*I*Yp z*T3=|)BI!Plj<4z2_XAl?LgADpL4kWxefhOf&A?u4Aii4M>|0G{b`)2Ne%`G0SQnm z&4@F0Li!Rp(?ncQ1Q5WLiE3IiaFc=LU|COJ1wS8>(!K!d&9JL^)kCj&21ua_buH-C z75rW*kpFn_c;WSV*~+cvGc$E<%mmhjfB$ood6#{)(c|=I>T>8K$M1^(&t`Hxgj-D> z8FArPBUBk|VvQ)t+glGkYdt(Yof3ITEF>eLeiZEG?J{@>H>Ud##vY9ThMjR4=T@2B zpZ)7z-@H|aJ-zv&yiBYIe3(CZIk#i2#-AxfgZ?YP4d3v_kASN^sIFIq{@AA{PQvd* zdsqZX*GAYbb^T8;eiR-alu^02j|SMW+h#I#+v2hhru z$Bc`IGjSayx*4^f*7%iT&Tg@X6WV%OTlST1*t;_1&JR-QsSTiHV$r>8RbA&UF4|6X zQ&q6z_=^`lg4ooO3{59CdJPAn{G-S)v2X(0TOUX#npqt{>74{po35t2xxR4>J#LTH zUq1RUhLrkXYQJJmIIyw~&u-1NIL%=n^3?kf+T!ymz?UXM8`fKz3pdQ3j+bFw^Tqqr ztkv!DT`5<>W2ugXS_1{)VOZ&HmAMmL3BykWpIX63CSkbM-_)v?7P(z4H|Fpcn{*Zz zFBeoNRpzm`gx(zZ_a5=Nt42l}wzehNuc#p8_pk%9fh85OWWYjfb{8S1g(911TnE0I zO@mcSYm`MgR5=>Xpe^b)2o4%|3}M(QLy7*R-j)LTEh|n$ljK}3=Yu>y74*Tz$@y>1 zTQ5Wa>a;#Cm`2zsBe^~&cd`CESiRmzSl^MpUPDrsA=rx+v14$S z6I%#Ka|ahqNj$-7CES(!v}s>$URC?Iz!waYE4EQLQQ98B9xMZ5$Xa6XN){pPC&y0( zL1o7+i0(@;8GHgdcDtF)Sr^tU=t`}z=F8^o7_P)*L+ta^0E{DWb}v5moInB33bE(k=Z4E#&X_t2yY3?YkWxq<;^3hW`b=JRMp=67iQv!^p?Y9f^| zG`Tn5Hbu^oOR!?fK3f9T8e*f%wbb*yPxw3Wq*ACxq1=QGFusc4*k5N{&$c zHWr57E^8%+#k*gMu+U*-7L3#1zn;Tm3h6Pmg}Zox+e)4)+iyTG=OH z1X7Bdw>Z!INh)Vzl*+8johtHs*3M5dn<96AJV`kWlk-u@1ryC_zBJk9V?RHG2zx zKE5gBAoaVTL59I;km{9GbxYLyp|?gZGZO2KINU&z4`sS*bcH1D+UTIBUgx+&eV|+^ z(Y{}DbwzIYWjVU0H58yd>VLHz5=?j_fY@Qt1AGKg4~@j%1@$`5Vm)bYKq|sih|@vW z%Qk#NG;FFbZ|7FgWe0OG6-*<%X}Y{QVb(0)MqX^a&eKpZfZY`gp_&PTRkjaRH-L}U zUpRvTl-OMNBPh0Bw5u)eqI61*LHbUksHfS`5Hn59@oyqp9mf$%Mb&T zF`f9v2z!$DL~G7-x1ez`(sy=Uybh@q(W~@ z6zie!{jECEXT)w4xt`JpW*k*dN+Ujg_Yaz$q{iO03ydfXE~*}jvkg|tjt%oS$7dhN zdSk*em2mN~51S5PVzb_CMQzL$&no6{6){Mu zg%(Jao^f^>tWmKdr(4almS0}UHm?A)K2s%3aF}@5*1_VDSU5_w_=*ql64x0*bWJ-< zdTX-VH&nfKfqwa<12;LGxH7zXCNruEBAUzRTb(O#Z-cKEW<|sfEYA(Ommx*>1^^ zozY`--7@MLoO`qY%Y3YU4XKUVf~|J7f-0D@o=Jmiv;C@!x=BsBgYR-MDa2$w1faF3 z(QDBGIwDMS&hi+=4iTY6ZSxJd>nw5FCgs~-wYRy}=Q+X)D;5`G#M;48>*_uR60w%O zwR>yhs<><>v~G~;8(`VS+GRMG_|ppp30h367M#x_s85JT4>ixi9@Qu(G8hH)*mbk= z`rNyq5nrbi0zocRv@B}kviL)hZD_;SKU$i&%;T$7G_M$p-I>?Z9IURcyb9j(tn4 z+J=$bxZ}z(jPfo$Hr)Fbo^HbpY`k_R924r2ke}8mFiXi{p)8G8$3yb3*0+#B=DI7E zObCX5!U`F*YJxSG(r}(?_>w1@_N^ap_3P-LCyR-vGg^WfZb1(jWvYgxRm>)mM3QK! z?+uDCg5?@R$3OnPv)MOXq}cgfA-117`medYe~r)mo7?=i&gNg9ovN+X|Bs69RvlOR z?Bn_P#=aRa3qT{^goII!Aw%!vlZ25J7ptOag*50de^cH&HU?zKB>lMlp(BAFOO5I4 z|FJ#1+#ik0(NWjMmkx^}MCPz_xOut$nAPKRIl2FK)p`Z8@1QLRzX!|BI4fA0#hBQ? zKh&2LXfYw;z!qTz@3^{`LokFV{EFf>-qA@83V#Z=z63OhOda=3H!vJ>h|b!%Ehs*M zO-a{wl_ImnRF~1N-4#3CzJn*e#DO16HhYDb*4$usw92tsgTx<#3)KMZ6i)EV*T>`% z#Y4=qcZ)*u`DE2|33?5gEn)YM%f&~WVNg{j&y`&AA7-Y|>+PepHBad(p9kr$cv&V$ zfXSa9wcO45wjHF$yrpK*CE25<ZA;!n)`98)) zv~`e$d7=~>apRXAcFYI^R-h#dAOqoxFa-m~m8}>3k0Z5^hqvhA<}Zu&G)y9d{fI9b zfH*XSd{w2U(Z>a{TNH@`AJ+P}CYo7#nVug;P;pK5e8ElU1pRAI1pD~had9M>fif)b zD9nGrLwv+I{si(rpqC!YRHEvGn1T3_(Hp-@=}D9VHtm^sk5aZBqNOYST;dy$az z_k7MX{LQ*;!Wr8Kk`5Qw&=NbENxFUIqTdeLBk)V5&uPCnvG=>TeMN?XSA10Ddt@5c zmA`4c;~+YWP3pp$s5zmc<1KL^iN=cj;A(A00;;OosRRQ(ln!nY(Me<)dkX${kaaGl zMJU4W%9G`)=mW_DM_6KD*+vq7xFc1EucCsPa_J)FZU@l9jW8@VUX7-9Syes4c~K3m zO&$2EUjL&5CGi~7O8E4@(h)%ZbFRdHINty4I{)SOs%bmTt0BK9VU5>|qQVdE5D@tr zeciwSO)64=ZWWO5FOn3_6RlSjSBclrJe>Q}{RY={Uwu%F)TG>BG~xU*C~WpZ@gltD zE3Rg|+8|w$7(SJ=m;z{gKgU7>2X2c!CF5{xlvw7SLZyIu6;yyuU z4|WH$F-UjgE}%@H|3 z;UT1WVQ3=Bl6?Y2MzDrlhr_num`*$X=1)fbKBYPM)i}q?O{_fL?2eY%i$BfTv64xZfyiZYs(MaR4rm14nI9 zXHkF)*@>u1Cm>Nw;*En&uBse;-_ zAO%x4)haHNSQ{$RGRnz00;q zy(bWtbYjm;T6h)<)?ptEeg?{4mj{9gy};*2USQrc{jd_+(kEnS)`p$K(%(6IA| zVW`rl{-o8%LE^d(=&z-_6G#2VTYSV{ftXD zl8)(ET}m#_t(Q>ebQ#LL?rCT-Y1qkzN$3YWKo~~yoCjyt)ehX zWME%aUs~|R$?Qi%440ZJ83_g~9xwM0>)l;v(AEoOLZFF$ zVVhN9k1X=!*5h4nmi+~Eb$38mBcsFgh{qJ+C$)@5*Xr!v<=>chfgqs!Pf{_44fDGy}yKSuEp;;AsKpK z7JZ;~%tR6#He_l5!Vh?hnY6k@BH`%(@!MDFZ@lS;ndjF`wAYJGNB<3Vq=|DhpC88(0 zpC6&SErRi8Iq3dYne?t|SWd@L%RhOn&v6{+nkt2Mio!9Nk6#TNw9IP}$P?zxfz!Xd z29@LlE{wgH${}_>WpHr?DNc{&>h-U&I5(W=?p5hMI#FuY(;E%YF7G=PHIA=5;qR_q z_Lx{_OpX12v;Ri!j&A9$8Dnl)0LdXD>r)$E8Kl4TTn*Kwo$+-wjKd}{ z$f-p+)O^<+=F*|?IJA%dDZ~KrtJVW%$Uf5bNCz})1cISixlhkEw1TBiPp;*-IE{Me zoa9-{#kHTtmBT5@QLZNx&m&mkPb`8+ChS7zdhKKJq3=p7q1IEn&FPWj-F`y;{$cvY zB*qy2b%OLC8Jt^zvGmceMM6`y^XWLfq<`FpeFz{*8CE%cv=UFiYFP1g+i&VN9i1sQ zyo~3Z3OvvyVJN!VT5c^-4NW1|DVJ)>>>p@keo>!DMhqQ6c^2c8Gyp!kH z)H~i8{#_GgS?f%fe!9IS|2=v8AG`X$G|~UVQcPCT{VRFP*QnX(Dl6NRvFjE^B}Qe7 z_Tw9gxd2)qY&`E1yCmRZ)Ktxsg6yO4XOVme{}b3tVT2p|7Zf-PSAwbR&ZC@hKDYPR zw>S8044y&|igv0#Iphp|x&phGq^ka=UKcB5HIh=U~OTOj4gq(-PE&bl z=_-F=$1k3E?g8&A%7sHQ_{nxez9j6!&HHlIM{?<(=)a9bwSsyS06PV1-uqh~$PVa` zbcMyRXUa5Fq5V2H`>M$k-V(Tq2g=`~uImOs0Kik@i-8VcFiRDa%6q76wAPJ)+fZ?n zG*!=cyq^W+du- z9T36BOr{Theb15sL90o|J|6){Xh&k;PfyToP3*KqZDI0M^afl*1(TSxPA0UzLdQ`< zt3QV#N&6*uqt)tDQmRW|5iF5@nH*aiO#P0hphfm27cqGF5366>-8L=hQw)!w{Ev_H zfBfUdf0M=k^7qwO{czRM-^JEP=S1pNM`D2Fs`H#FCR~7TGw$V)d*rfs>r@Vs_FAxC ztw`kK%#vnD!?mTP^JhYeiy<;nd{`m_idbRDzo&3K-Av)ybzQ3?_wcabNH4W9F|d3F zEFO7|yv^F@K4)8xd$`K#s!LS4?rB3MlKW8!RLlkjonamXp^9k4x(G zHMoCg-dq8;SPtHzT|Z*> z&~JQI&AZ6ueA&WlcN#Q&bwRv^htC|k;sua;(g!o$rH{R(d3)#x?8csAf-g*0mt+ea zjXjoHoC`;@%Og({xHX!8&uuqp5ya0hS7IV8)@Wq}Cr1Ae2bxH-MFi3JjwV^4Lq(=& zQCbAuk@;LZELNC@z&JT5vcW2Moo zgvq2q$huEon^r^~v7N!($O?J>%2Jm$Q<28BvTGbV$RZCGN|c2m_Nfhi;J(5$YO%P< zRC0ZC21||uQUjv~?x)UI-N_|*3>l7-L4f4mr@u_2A0CJR-<(U3%p9XJL2?k_LH zo1(x?jHJy(hj&{vX`UXee<+|PNvqB;4M+DEmBSSTB@#L_tKGzzsFy)sR=T!ZN*`Nt z+ZR=&!e&TRSE9d1t+`$W zC!^%@mo&$fqlV+lM4UEMb~QdzmgpX%TlhDT!0fZ>oEAvo%jqZ^1Y86wHL_^V`9Jn8 z*j*kJGeIj5^I9t5OlUJL^1h6tFOvl+;~9z?gx=9X)_4D3Xx)v|RRLfqZmmADgk zC&U%v?(Xg`#GMFncO~w`-Q7coCnWiYcex)Bc=z3^|5Qz#nX2iv+fH|%-MiN+BIU8f zsx1uNbp+`mfG~qk&VgyB*queUqo5d4*qGgLmZ4d5%A(hzlCzS;hySc>LhdOf8ij@n z59zDn|Cz9KZujAqU?z~Y_}dpkk{g~d!hudNW-ofZ>uwno~Nj+-6RM*J8$cAinVIWTSFel1zyFNozGc4XXiWeC2b z57jKMz@}UGX!e8AA`^fA(mM6ooYypGEN3%g`>S2ChK8V`ZQKHPzG zf&yO>!;f9SgWYahQ)ca1GnS8<8?)_;KFWy}ixTo4Xq@u{!7$&ojy+i{stN@Rc52+j%!C@rskk1&J$We*H-07c?5(wJuJq0m_ zoMLlG^1s71cFqUG6>PQpC>E&E}-imBKbcL}- zl6nU;>qLJ@qAj}&dMW;LYinP+74*3~$b$R~;ZhBpaYlay6JB$Ok)A!E5ju-Jpg6^{ zKjd4yt_UPK%q?psgOIX+*LFTT2MMCHo3G`@!+)pF4Kikj`` zA7LcO*~BKaqn3Z>**UVXn%09J72X%?&@)+}`Y`z*<+gmzMu9c4*9fzFh#oIK& z7rd0U#YQa%TW5(^iCA`t&$F||S!;y~N=dWvGO>ldWy3|5DDW;SKR_UeMC)H@tVFdl zO5VNJ1V&xq2Nmw+rw3XRWNrpIwpi5{iPKz8GID2TC_lCwfK-!8rOF?V$)F{=c5vXD z5VOgF?A<|8!&sW!Hj% zyOZ#SX306CuKg_aj_&&SXr01+mNE~-wM|J%uys%{;ysZdDY)&a=dX*pP<|FOH^8C} z8nCG2{N2&@%Er<}U)K(BvjW6M8tdEsG{rv&m`sb2lyuH>Q>^A`!OXfoYansLrsBs7Z1TwdqO- zoy`vIreh#PsJ(Ws%}+eAT{!h$Qu^Y}H7}MyO?#b5>FechQEe(8K&)$HFQsyEZD`~+ zF(VM*7j9B=(JnG{sk%FdTOzcZv^x^HOFAQUy+|5|JPj6sbQ<9wfkPGeCiufv3-85r z5GMsu;7jj$KOIkrsqjlkbllRC*$}%g1_xSHl2`RpxKJxKd9W&q%b&57T5!YOFB;S1 zF?jZw!ghT0gbTM~_f2yISF2cISD-gM=EcH%b*`N^l9FT|7dCRl?VCO%2n8x%g=~up zorjkH?0qP*8{{B^M&#PL+P*ayt-IjFn_UUuFRy7pSN zJ0za2Dfd=~AY4L6fW$;#;_4Y#s==JOLjpj*({r^uA^G~P+odSx2@SRsG#IjAqU+8` z!_Ek|&BlYHPiGx+Jt2fECSS|2&573k3pkmhvdPhwTb6U$4 z2ZOD-)#o@N{>G&@+ftrn#U8wa2Qhv8jsgRohbm)@U;Vmr<9hs5F>^$p?sFWIMN=%( zT5$UXfSGthtjrvGB_Zx}0xjdZHadYO^1vh)1)FV#HR!;V_5yzj~ISjjXhco zu2dub`p|}E!_mWAV!47G$Eukc`B`_Wz%&u?1yxyC;TS4APXw1Zj{IlLYdSgp|69i4wlZ){B?!ljZOwzS9wh#alq1r34@tP}}zVc_fO)EWP>3ss( zb8+vb5C>bblO3~@EfL@2N0m%_5Xj{}g2q(6L#G?@4n~1L+ zLgU&z#SshE5&G&w6B+lm=pDt-Gw2QwM4p^83 ztEKCLi>dlv+htPHkQ5x*<;KP#w`*C;^!&l;NsZ(3*XsskA?8ro?QytU&zrBpJox=P zWmxyL2@f*(2b)>)oJViR3xZWQaMJ9IH90X4r{_AglBSt2jZ;&4Id}FH+5=>6UJ7hP zbE2Mpcsa7;^YXuVdL&-6cF0vHcF=zEWL!#SnodMw)$L-NhIaiHd2bZ%Gz0BEdS%?V}@Pm`r+z z<-+S2q)VA}r$elUpn82yS7oSEf+$zC(poLJCh8?S7doRgwOws$FvC^Hdg?LjnBn-> zyYrI{-cng%z%ijtf$K5^)f$?pD zf1_-{byG1{zpet7eajqV@?y_h_1Q2-;fl_! zq^i)v3__+wC4DB9dPXGkB9qW$TEe124wPbvLvww4v$=s68o=qG1{5fBiujA>H6%mb zUD)N%S<=_&hEQr%(&UQf6k5GdDB!W@D}AG>SgLujy69Ch7^DR#3**z#!;;hm(P)k} zQDDF~Boj4Aa}N?1?W55oS)psN8aZp##%cs0cZPj z$dN1YBCG6N3ucPzfb?V-#vI3*0Mm!BcPg=hW&}Id@*WK#*-)lA$!zuVGe92hm=_bM z9YlfS_-Nc$ULB-x$3IOc1#4)5Y(10I!T?^!X|AOVjqI$&aX!t&#!bdl*vJ(d4Pbi= z%!!FpC@!4U&`1`2h;k@ikc! zQM7jR0TT=x^)APwy|EjdSG8gYh_xR`%-uCfP%4w(^`;5TKP!I8PS(}GCsu26z)Fv} zC?8u9M_sAkj>IFnBuo zyZtQ@caH=FEW_-CQ{*}!BO)=ovR`9h*r6|(kMcK8WYUeAgDvqpGKR~3(V9X%ISlE{ zi=WdD9c8x|g|8pX>}*EHcX`Eg1%v?3>Xe0P+Dm4=&b3Pc?P%P*uximdo*B5ukhh){ z;mdy*-GlW;|1;h)H4HCtMp05>;LA t9m@SZ!E*7&jsr?!t7TL-WYI4eM@gAug8 zmYdImd_$moc|Wl+D8f)Ox9p>-vTa~|_%Q2qvp&29w$cF()B3LM?Pv3^!oHR}TtG&o zlDfH&A>Hrv!B+ag{dZsZo@@&OnX}MMFiHk?89N78gbcsa7aL?|msUy{d_N{Ox!Re1 zKKoG>8>U7KK+}Q|CGiSY zBiLkThmxruWxvQ{suzTd3|nw8GJ9ZoBT}&LCY)3IMut4gSTls>>5(;F)E$*=m|5LW z9hA=x`sj{ieY{t(w-(l3#W26Ra}DNucjF9^RN8zF3{0t{K?4oLLukz2gBi}^A-CJ+ zO+;EE@_fEFi4dhp6PLYM-k;rs&h?<1DX-T61zfk=00LrkTyxQfh`_8yAq0&sIH}F} za~%n`$^MWPI}#nMx>^Xav8i-1EV*d1d9uo4SWl=U=*Ceu6P1AimL2p`;pre)TSuA6 z*JQn}3n}ct{t9*^ID2$9(GF`SjDYO4BLj?uV6c?Xl!dhl13wj*Q_4z(Dt(bHavklA5pHE6LQy9-M8P1-t6t+zNWix z-izoiiQtEaytHn%$}IlG`9V>Y*JYH})3G5Y%+ohLkx56L6n+7%5^(P5>A5+maMQpS3iQ_c;ME3ZbVpQg z*qu=77cF|QikGY}GJPAzaFuvP65=>fS8i|(u9O;DL^t{u^yGpCRh#&i$sO#HvQ*Ic z$2AF582U^eo28jk$A*vA7Z+7#rd5ctLnV~hsm(bDGf_KKEGD<)HJ$@& z;y7pIsm1#6;)yRUN#ZEt&lz;fUBG-OTR@fXLt;J)D7I2>*7T=@i9&~D6Y3BL-=-ee zWQ`B?C}k}e8gU5W&Tp4_4y`!eV3kgsIG-I|Iut)2)6`(=~RnoW0iNLI)Qt&-%E z1j~+p`TVP0EKwqCQoI3osA_hd6=A&oDDz?mtZbt`kk+BjDpxd-+J>h&uCJH&j%Ny2AShK8|D zBUN7KwtGD1Fe$0W`QSk)Mc~NAtg)hFGBgLd8s!ry zE|e!24Wlf{14}K;>lmj%8v-u;U^Lp3{BJC zf3O)Gh@9xd!@5uiDN)|5qY78F2vK~&EfA^m0C8J+RJQuqd5+QGS8zaZ{^>ckBkva5 zg*?CfT-E0Odx1PH&i4r-GgtC*@~U30#!`aL_~G4Cy+@8$W9)f?Zm(TD@+?QMv1I*M zCIk)f*2%x7cR+G8pCW8sP2`ZNayG0%tc0$u<8dA!gahP}p087KGuQMSTwRVbBOE^a zXeaz??`o6oIIF6tg;gJs!T_RVd*?Z<5B@(&8MoRVXW+>o!!FI<}`8~a5I z4(U<78*wHBDa$f|KPz;HssLwWm6+9`TxLnmo;QQ3&C`22abTkIaOK%#}$OCR8st88PA$X{6?t>3x|i;{Q(coN#bAl;%FEh_L$tYwgwcd}$UC24(})!{3>9?E4W zsjx+EDJ-7|?DK?O{v_@^faffTc`AKdYmPWW_4#@77xnw<>VoEk5m2{jV5J0>XP^fz zd(8nMD6N-cHi_98BY}G_K3FSLm`(z9B3-gmw)pWkv!+1%4?~s9i3NqVQS@)>(5nUy zO`E-Fcvu8UupgJ?tA0W7`pCm8@7i4kV?y-et%DyKyp$})OZR=bwzBdy_7WeI59MmJ ztrE^5SK8xHGjH3EK3yER+XYMR8WIs~W*WtDhdO9Mg5@re?2%SaguL{To$56GdF}O(gN$moKGQ$q`- zESPgF*T*p}r+qTNwfKB_LMKvSNj@@k$U{-61c9bGvDGOEXk=q-k>q26WQq7C_!1d{ z^9Rspm$rUmcMu6Hgnm2%qi#~sjyD>&cr#;H4dKgcn&&T8BzQNK zcYD8b-uub=NFpu6W$Un0z7?JUN+i{@CA?#Bfo^6IYfEbtv?PAHl5Y&uM9y%><#%~C z88S6`LD8`!$)YD12VMya>VYNu+SnRqbQY}sk*6iJf@SqX56OpEWA9~v{2j!NhDVZz z5U&W*^^NK+B(v3+Su6PbvWUguA?R&^1e16&hmkqAXZ-lt4v?byG#$OcnG^U5gBDlu8`Di%jjGDx$l5$~GG=bM#7QSIyu3xAk+0hq&o~a% za&~|#ze1$ffVJno9#=Z|CL^*X$w3<}dxrN2m+6epca}i``Uw4Q!P1DsJ+rw2WFF*| z#Xa>s_T{!H@3UKWD$j8H9G8>MT440SUEX$L@J0VmX?vMvyPm$&0k`l#m7;rfkWuD= z`g$|u0|(E^HWy;f z7OHk4UyIR9j0vuFLMDr`4tuZx-Sv2=Et2FK(%Dagqg>}~T;+r)P&K{NI_5)qwhRq} zLpQ|?yuv$Xbjw6=FPJRr>21!FJ-BO0LG&QwO7BP;W&_Q{J;Kf~EBtBWgSfz*Q5=To z6hn$H41&=oe$O%=2lPX?TptHEI6p+H(j|7-{M^iYA*gv-lFWOwYh@cE@|8fTn-hRe zj6Xo*7R`Y-UC~fEKP?pR7GFE4`%$vZQRQ&p#dsR}<3~B0kH$#Rr2mXG1I+|b=U{HVAvEvpP+sCpyRT#gBax8Ao_)n?Sh*b98GbjN?9C*Pl>NJ z-3WsvvV-y4;q_nE6}_*F_F<5A`NVOxxWcisY`c)r)_M>0swV^tbpoq0agSVFnW2a< z+!>Y(O(9N^hH-P>qpF{~Xx)jm)2SOBwu-QRYu;eVeu!M7+RW5`#n7M7cJMTHm9=xz zuJTUm9bwD9ItZOu=dDAPL1=#Sc8q@g`b>lRR!6jpo)oycOemq}j{e)wUQ6KKtDMGd z=UNqe=OX=B6TC2-P)ssHvh@SX1D)8mvN`N$===+P^o*L$-77W|TUwoq5PlmhN(QW$ zuQizUY&2tGp0}b4eyH!DpNwCSGiJ=hVs(vj?UHzr9ZGw(68YuR&2r<(eF52(GMJ<5 zR6GtHo_Mz+7=1DBT4HSfRyk^18t4rblN63Vq;Kt-WoYAldvpoI{1y{k=n!#WvzzAN zd;H`O(ts_YTc(qmowhTV)a6-idBz@lRJJcFJ<{dWmb!P}UxPfn6CxPv0{@&9=9ot+$Tv`W!)NW*nJrUNpaIfGwrMcw%6#HX$smzH#9=O`er{lr; z4K>^k(duxHDbohK3l_FX+U=%+wL39YI!zAs1N7>L+%qYZ<_shzT7vX?GiJ)gCv^^f zkMSq$0uEpH7w6VnX*Vd6ARLdp_*Y)Ra_LjJZ8dh3alC{8IZ`uCU#U*!v1IQkIX zQ=>g*)eB`?g!g;H9!~x&DG%b!EdRn<#*B05Z5W#5y z;e-#fqA?mK6#7R7m{S)`5dN&jYQE2Er!o6?P|}tzcOII})mx*zu2e&kK@r**oHiKI z+tCp;FgjWVMos`_C~6qwrQD2@1sTC>&h)p6y|7XYKsS6dKdBx!eGQrUI zfnxA&>X#ch802~|3fWrif!J`J%?WcMbDj?vDhzGJ(UN%DtI&BK0t-AM5&^z(hSfNP z_o%UttN|ltZd_~31f~_*-GV2R;ZF27DB0;~B{p=%c>E_|kr}|`TyF(KhDBFlV?;Z$ zlC~OjyWkpElYLUsh{>5o>2ZhoI>VB^&n>dN>Z3c%7x%P9)*F+I4HKn{#uJeOisPTC5M`VoSXwcG77#2;V>|~+1O-Ry=CbdctWt3Awn_a1l z$}AL+G}7WO*?1O|Tgi>D%aRNAIii4DX3vdmyX*oBm`Q~yVDZ9cVS4rv!?AIF70eBj z@Ka-VM;!1|JNHl58m3EvpKT+rU1X%U|fD{8)Mk z+c(z`y`l{5K(vk~H?W`JY@5sV{%C96Q?o-$na;V;3g@y)WSHiIBTIURkte#l_d*On z+Xh2KcK+Szi#+|Iw`yIwm?wgW(Ft;Vay>L}=D}?&_G)Z7^DRDky#FM6qZ0iJSxDm=xV$_pzJf zb0kEMC3nrqD2)vFlJxav_GW?_i;P}|P|T!1GH7;+Lc4k(cfOL(2(@X0g<&PY)eh3WA4k*+$S4=^WrCqw zYoL^Z@LmHGL38I{`GgTVW_J#ut7XR9O)}if|K_%sh@McN$Xc&6gC(Mb z+yPtqpAKK-qKLaCrE%P)ow%)VFtt6pJwAJjNKL8t>Xn=np^pIkEqzAzRzOIKI89EJ zS9%XE4VksN$H|9!>b9%R%AEDq5O63Y*C8`&W&XU%!OO(uFMb8eeh0MFy9H34I$DEk zPzH@22|iW*G=gO=5#?c9jJYHd9Y|WL{LF7=6%f>G4&oM-5z#!yOw4R|P#0J!V@hUO z3@jK$`)o17oVk4BHmPfMcLO^2$!1LRM&B^@Ze1ugjlEUUd~MFmt*x%`!r01E9_tl- zB3){N5S|QzP%5{#U2-ZndULy4^3(x!#F&ZIpgesXZ)8kFY%y&AgQToYU_+LU$rv_h zLE(~($=8M`T#TmneILDXdOvN@=lLeeIDto!{aClrQ&zZDP-HSir72`=iK-Wgy)(u@JyUQVqRi(h&z{#F>;SFJA2tds&(i# zzFd-Fi8~eQl&3VheC%-!(ARZMnE4QxFcJ}P97Meg+M=HSE`VCJVwvNX;GLbQ@moz_ zsK@@+q7F?{<`#FU@s$2i-)!&x7vqjzGKerlGOi{ZB?*+TMdBRz@|+-Yox=L23A5iI z-W|R#8>Lzyq#zdIAg%@|O_%CS?%;RUL=|D$(4w{xdU!4ClGIl26UOj{zCqv;fX8&l z50EEc+eI8l{OWUAplO}R>|;`(@IK?Zw?F_78FwmSeyW!e@3iQ^F6MDP<|2+}4LqMK zW<%R%GzzDii~&{6Nd(bYIhN#1bT@p}-jRAcij0G}^%Xw$m;NPY12;@NL&2Wc6x7(~ zt1&*$KUBc$ebr6qxq%CxtNqA<|L*b0^j+ItZkq^r3JL+IS^pK^#b1vBzoWK|{$Bww zKk;3ZC<4~1atPdYfUs+a3e+r*Rd5}|MieNPzI-So1`^ohN#>89bw_IGbxqsH(~+X5 zkY6|8rG>&tc)Z~CQ`O_u#*>BDGe$;+l5F!Fw~rsbUfhFwITw>hb-}`NR(>%Sc%PAi zMaGaz2rk%N4TcKXJz*iC&)3lsjwV#KO_4sHl#JJ93`@`$qhJOpTQJBnQ1|cEa58W| zgEx3bxXoMFe5iqMhhC~lLEZ_@1U_0MBrRJcXz+r!Ns$j zr{tiXZD67L#fg!7SG6FM*uOfWN@bKGh>6oeSD`yQf|RC6Wvn8ECBXmHR=8m+Wi8Fx z&6X027!%ADv}6qz3={dr%a{0AiOWY4aPu|Y@*`1%k939w>v+#G$U2p|xK^~5>bG!V z9cavEFu|N#9#+HYoctGP&*%mf_Hy^-@{`WghR>T1J8(1?gON3a8*=C#2H$b-&6!<& zNJ}?;iIX2ThW$F<(GaB5rrX<2?FF}R_A8^v0HeyCK59fF308Bd6JN|jY9bL2{4rU6 z+7IzxXyC(#3Azm!1S(**J_H;JXWo;r5Oq02zJGQGb%TV;l-I_0GrAVaU#eIUNb;U{! zA_jvAh}tv!=8X7#;QuMY>q(GaxSX_PCm(`4AO?G~tdRT@5i^uXnKY%C911WL7D%iBdVHF5)k%x?_RiG-c02b7t{rYFQYwi&bSZ4s3Ut2N z$FFgeYi$^%bL?CEkgmA0&N{$lP>7t7gMOY^Nd*nQOg`A+S&98D$X)b68tT(|Q6?gcp=ib%I|T z?Y6s;pMzPqnY=7cdmXpMxhBh4bBj*eFy;cOu~MqyH+VFXQs#H;3EeU5u~Ws_*XP`0{RA)Hu@sQHnw*1_B!9||F5^-ZY6VhWM#l9`ARG6DkCx2ceS%(zI<8` z{6%~S(1=k;!RB$Svvtxc6H|IKb7qB}S-e?~9V6Ag@dcOahPSzo?|HK)Y#ntW$jU!j z=e;=|YycdZZ}^n%diij1Vo3*-WBsN_bto;{KuZL}76%g(2~D47RSih8e&jSbk;b+d zVip#YQHf(3tbD{;z6Xrw9Yc_GL~0m9E&CUoI?UUnlM5HS0BssWwRZ~LuN{lj3N@zW zRjZWb!woh=m3WZ=opG+T{_>0vTrZ3Y8aTL@DC(6VRd3^&zek1B-@M9 zD)u7{B!(^HvKSF2>p4K4fcfbAbtnPPNIzwR3zSNNNGEBna3`8Il6}phx*tjEVaE$94$ir@_&3|3bvffg+)Roa9a7j8~A z!Gwd?@K??Q;Zx-oCj0TXVkn;k!Kn05hYjjyWhRE>lwB93!C|&ReNVM84y~fny#@Cl zW~JZNy>gj1wJS>odt)eon)6KaAh4AeKfd7=+K8;ujKMY!TT zpY4j5x@!=;4;xmg7*@eTGRw(m=DQrq5%{2=pc2{|04arJ&XAlP4gc(rAOHl{J#JH6 z2kSKgiE5*B{mT-uNn24`hfJk5t4_2udIt1ys7?mSeI`S@{xQk07aO`et{T>E8r^}D zWl;`>dmL`*G;;gBq^BBMe5qR9l>3M{UQRCz3Gq6i>xJv-FEYe=+@$Z>V!q=4I)=mo zaV33=to{lZqd9&bqvf4#?exw6jZYyhW>BJ&4<+E!Y>|0Q?X=01@FI%ldK4P^ zYr0o^9?5tU(Im)Z69UT;%0AHe?SV+-#s~%cU8<=}XP+L2QyZE+n_Hi?KQl`pfDb1! zL&;M08wNH*%@ii^9C%6g2~uzVHj1xyuvaW|-VkqDY6&sKmD48f^@(jLry!LIvrJcU zYPnatTn6+)H7G8Zks2HmxHiF93-Y2UAtspSapNSmXsAO2n>%k*uVC& z6f9_Fz7X+7nT%<(EeGegSd|+D4j#!~uf$5CLVjm^N5==)ae$Pd+SaXr(?_MY^&OyQ zXoZ>rIVQ2nYdx>_Vr|PxqO+p~9j3|VDlh`vUu3I674n!Ksy%}I+N89oMn2$x=4=8u zix_`z(x0Z??}637Eid26uUL-1LV1v(M1i(#UsPa5X2YRp-FIWckS0k^j53EbfOl=; z>uiiuw_TvU<-J)CCF8jUzXrT>mA+bG#3@qrtBdBD_QYwOfhQLR@hJRvQD5fAl~8-mU(#t@K|O8wal^ULicls6*sD zlK}1F($UYPtp-IbccN5$@tQ(Kc#gL%UZ=)?atRBG(1kkHw)- zBvU%*H!`YR9j@FA9jlr++8*5Q;0OYQ5r>1A$B|ISe1gO(`RM|zB-_iq7BrZs1lkk5 zxPW_vovda3g6@FvAjIe=Q!FP12nI&e#=|v84Eu_lNn?hKqH|g+2u+J973II4i6l1KOZ+1tel?TSo>>19YKLcYgzZc)c@+pD2^K-#`VSM5tHu6Gc7EX9UjLzpxcY&>A z4PnL5cGhgp*eccBR}f($1rmWKMqxZnOm$K$_(`#BH~^6C-N}q`>0yO&FmKs%KIJU{KDw>Tk5;q z?QT3gqd~Tv-8J+NpHKKz;G**g`y9sVtH7<3 z7LGnP;XuWT?XM`a9^url?|2<@sLerFSLuVyQV*tOx{rBtL28JyHGFKq?rNaer2wvn ztc!eqj;1LkZ}c_iZTAqIZs|_ooB(9K70`>!$koJd(2@@v=mN6?CT;!K6|-kv61fC*%7P;nUYmYO(fU2bcLJqaiXfDiHaHzCICue?pJ0k%1t+DP8V&|t8cMer-3jvlE03V`XEII)4@CS?Hf0yB}m&~Vl zAO$W<8i2gY0aDZcg7+5SEB*tXsExLsnZ6=`eqPMdTwlu4($wDS&(JvQnhV_kkXt}6 z{k9?e_f_o;4iMw|12lm1*Ua7)aIQ?m*i4^aS6AQGR$ALa+wgCtg{OHRg4GiF#-M!z z@aO%ScU*v`=^qRz|E0_UaCI0M8`=ZtvjJ4{f6lv{JFf8-ph_?Sd8hw7GKuDgZ#G`Wq5(ul7z7{3GgL55;%v zZ<+pcMLd<<{TsU4J67h8xZkVwzYRZ6B@Tb!*(&}K@0X_kZ-R$UYvZYW-VZD8%73)- z&m+!L)tn!2Q*Zun^87vk|8WBSIe*_ax1Orr`~Wm~``N zkC|%!Qp#@>Hct~j6_NQnd9`=)?}`5o6ZmPl{>1tE6#l6&$Pai@z2EZo6YTewONQTj zI; zFTC?l;h$2b|A2pI_D}HNTjHMx)SsGq%Dwu-RGr=# zgZ4Yc(NoN)gbF_}J3@ZP{P*+ z^KkVvruGNsN!I_y{6mE8(@Z}NVEkcVBj;Zj_<5B2a|xb?kNq&vlmDB6zh{YmPPuuXtC}87KZ=LtMW<`6z~@KO \(.*\)$'` + 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 + +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" ] ; 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 + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/azure-android-client-authentication/gradlew.bat b/azure-android-client-authentication/gradlew.bat new file mode 100644 index 0000000000000..8a0b282aa6885 --- /dev/null +++ b/azure-android-client-authentication/gradlew.bat @@ -0,0 +1,90 @@ +@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 + +@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= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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 Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_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=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +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/azure-android-client-authentication/proguard-rules.pro b/azure-android-client-authentication/proguard-rules.pro new file mode 100644 index 0000000000000..51508f487ebc5 --- /dev/null +++ b/azure-android-client-authentication/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in E:\Users\jianghlu\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/azure-android-client-authentication/src/main/AndroidManifest.xml b/azure-android-client-authentication/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000..281c2ce4932ba --- /dev/null +++ b/azure-android-client-authentication/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/AzureEnvironment.java b/azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/AzureEnvironment.java new file mode 100644 index 0000000000000..dd8a1a2dc9de9 --- /dev/null +++ b/azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/AzureEnvironment.java @@ -0,0 +1,86 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure.credentials; + +/** + * An instance of this class describes an environment in Azure. + */ +public final class AzureEnvironment { + /** + * ActiveDirectory Endpoint for the Azure Environment. + */ + private String authenticationEndpoint; + /** + * Token audience for an endpoint. + */ + private String tokenAudience; + /** + * Determines whether the authentication endpoint should + * be validated with Azure AD. Default value is true. + */ + private boolean validateAuthority; + + /** + * Initializes an instance of AzureEnvironment class. + * + * @param authenticationEndpoint ActiveDirectory Endpoint for the Azure Environment. + * @param tokenAudience token audience for an endpoint. + * @param validateAuthority whether the authentication endpoint should + * be validated with Azure AD. + */ + public AzureEnvironment(String authenticationEndpoint, String tokenAudience, boolean validateAuthority) { + this.authenticationEndpoint = authenticationEndpoint; + this.tokenAudience = tokenAudience; + this.validateAuthority = validateAuthority; + } + + /** + * Provides the settings for authentication with Azure. + */ + public static final AzureEnvironment AZURE = new AzureEnvironment( + "https://login.windows.net/", + "https://management.core.windows.net/", + true); + + /** + * Provides the settings for authentication with Azure China. + */ + public static final AzureEnvironment AZURE_CHINA = new AzureEnvironment( + "https://login.chinacloudapi.cn/", + "https://management.core.chinacloudapi.cn/", + true); + + /** + * Gets the ActiveDirectory Endpoint for the Azure Environment. + * + * @return the ActiveDirectory Endpoint for the Azure Environment. + */ + public String getAuthenticationEndpoint() { + return authenticationEndpoint; + } + + /** + * Gets the token audience for an endpoint. + * + * @return the token audience for an endpoint. + */ + public String getTokenAudience() { + return tokenAudience; + } + + /** + * Gets whether the authentication endpoint should + * be validated with Azure AD. + * + * @return true if the authentication endpoint should be validated with + * Azure AD, false otherwise. + */ + public boolean isValidateAuthority() { + return validateAuthority; + } +} diff --git a/azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/UserTokenCredentials.java b/azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/UserTokenCredentials.java new file mode 100644 index 0000000000000..f52654e78db53 --- /dev/null +++ b/azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/UserTokenCredentials.java @@ -0,0 +1,190 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure.credentials; + +import android.app.Activity; + +import com.microsoft.aad.adal.AuthenticationCallback; +import com.microsoft.aad.adal.AuthenticationContext; +import com.microsoft.aad.adal.AuthenticationResult; +import com.microsoft.aad.adal.DefaultTokenCacheStore; +import com.microsoft.aad.adal.PromptBehavior; +import com.microsoft.rest.credentials.TokenCredentials; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.CountDownLatch; + +import javax.crypto.NoSuchPaddingException; + +/** + * Token based credentials for use with a REST Service Client. + */ +public class UserTokenCredentials extends TokenCredentials { + /** The Active Directory application client id. */ + private String clientId; + /** The domain or tenant id containing this application. */ + private String domain; + /** The Uri where the user will be redirected after authenticating with AD. */ + private String clientRedirectUri; + /** The Azure environment to authenticate with. */ + private AzureEnvironment environment; + /** The caller activity. */ + private Activity activity; + /** The count down latch to synchronize token acquisition. */ + private CountDownLatch signal = new CountDownLatch(1); + /** The static token cache. */ + private static DefaultTokenCacheStore tokenCacheStore; + /** The behavior of when to prompt a login. */ + private PromptBehavior promptBehavior; + + /** + * Initializes a new instance of the UserTokenCredentials. + * + * @param activity The caller activity. + * @param clientId the active directory application client id. + * @param domain the domain or tenant id containing this application. + * @param clientRedirectUri the Uri where the user will be redirected after authenticating with AD. + */ + public UserTokenCredentials( + Activity activity, + String clientId, + String domain, + String clientRedirectUri) { + this(activity, clientId, domain, clientRedirectUri, PromptBehavior.Auto, AzureEnvironment.AZURE); + } + + /** + * Initializes a new instance of the UserTokenCredentials. + * + * @param activity The caller activity. + * @param clientId the active directory application client id. + * @param domain the domain or tenant id containing this application. + * @param clientRedirectUri the Uri where the user will be redirected after authenticating with AD. + * @param promptBehavior the behavior of when to prompt a login. + * @param environment the Azure environment to authenticate with. + * If null is provided, AzureEnvironment.AZURE will be used. + */ + public UserTokenCredentials( + Activity activity, + String clientId, + String domain, + String clientRedirectUri, + PromptBehavior promptBehavior, + AzureEnvironment environment) { + super(null, null); // defer token acquisition + this.clientId = clientId; + this.domain = domain; + this.clientRedirectUri = clientRedirectUri; + this.activity = activity; + this.promptBehavior = promptBehavior; + this.environment = environment; + if (tokenCacheStore == null) { + try { + tokenCacheStore = new DefaultTokenCacheStore(activity); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + tokenCacheStore = null; + } + } + } + + /** + * Clear the items stored in token cache. + */ + public static void clearTokenCache() { + tokenCacheStore.removeAll(); + } + + /** + * Gets the active directory application client id. + * + * @return the active directory application client id. + */ + public String getClientId() { + return clientId; + } + + /** + * Gets the tenant or domain containing the application. + * + * @return the tenant or domain containing the application. + */ + public String getDomain() { + return domain; + } + + /** + * Sets the tenant of domain containing the application. + * + * @param domain the tenant or domain containing the application. + */ + public void setDomain(String domain) { + this.domain = domain; + } + + /** + * Gets the Uri where the user will be redirected after authenticating with AD. + * + * @return the redirecting Uri. + */ + public String getClientRedirectUri() { + return clientRedirectUri; + } + + /** + * Gets the Azure environment to authenticate with. + * + * @return the Azure environment to authenticate with. + */ + public AzureEnvironment getEnvironment() { + return environment; + } + + @Override + public String getToken() throws IOException { + refreshToken(); + return token; + } + + @Override + public void refreshToken() throws IOException { + acquireAccessToken(); + } + + private void acquireAccessToken() throws IOException { + final String authorityUrl = this.getEnvironment().getAuthenticationEndpoint() + this.getDomain(); + AuthenticationContext context = new AuthenticationContext(activity, authorityUrl, true, tokenCacheStore); + final UserTokenCredentials self = this; + context.acquireToken( + this.getEnvironment().getTokenAudience(), + this.getClientId(), + this.getClientRedirectUri(), + null, + promptBehavior, + null, + new AuthenticationCallback() { + @Override + public void onSuccess(AuthenticationResult authenticationResult) { + if (authenticationResult != null && authenticationResult.getAccessToken() != null) { + self.setToken(authenticationResult.getAccessToken()); + signal.countDown(); + } else { + onError(new IOException("Failed to acquire access token")); + } + } + + @Override + public void onError(Exception e) { + signal.countDown(); + } + }); + try { + signal.await(); + } catch (InterruptedException e) { /* Ignore */ } + } +} diff --git a/azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/package-info.java b/azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/package-info.java new file mode 100644 index 0000000000000..2f23671d5be16 --- /dev/null +++ b/azure-android-client-authentication/src/main/java/com/microsoft/azure/credentials/package-info.java @@ -0,0 +1,5 @@ +/** + * The package provides 2 credential classes that work with AutoRest + * generated Azure clients for authentication purposes through Azure. + */ +package com.microsoft.azure.credentials; \ No newline at end of file diff --git a/azure-android-client-authentication/src/main/res/values/strings.xml b/azure-android-client-authentication/src/main/res/values/strings.xml new file mode 100644 index 0000000000000..e2639a86a6c96 --- /dev/null +++ b/azure-android-client-authentication/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + azure-android-client-authentication + diff --git a/azure-client-authentication/build.gradle b/azure-client-authentication/build.gradle new file mode 100644 index 0000000000000..65c1c86a20a36 --- /dev/null +++ b/azure-client-authentication/build.gradle @@ -0,0 +1,98 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.2' + } +} + +apply plugin: 'java' +apply plugin: 'checkstyle' + +version = '1.0.0-SNAPSHOT' + +checkstyle { + toolVersion = "6.18" + configFile = new File("$rootDir/src/client/Java/build-tools/src/main/resources/checkstyle.xml") + configProperties = [samedir: "$rootDir/src/client/Java/build-tools/src/main/resources"] + reportsDir = new File("$rootDir/src/client/Java/build-tools/reports") +} + +dependencies { + compile 'com.microsoft.azure:adal4j:1.1.2' + compile 'com.microsoft.azure:azure-client-runtime:1.0.0-SNAPSHOT' + testCompile 'junit:junit:4.12' + testCompile 'junit:junit-dep:4.11' + deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" +} + +uploadArchives { + repositories { + mavenDeployer { + configuration = configurations.deployerJars + snapshotRepository(url: "ftp://waws-prod-bay-005.ftp.azurewebsites.windows.net/site/wwwroot/") { + authentication(userName: username, password: password) + } + repository(url: "file://$buildDir/repository") + pom.setArtifactId "azure-client-authentication" + pom.project { + name 'Microsoft Azure AutoRest Authentication Library for Java' + description 'This is the authentication library for AutoRest generated Azure Java clients.' + url 'https://github.com/Azure/autorest' + + scm { + url 'scm:git:https://github.com/Azure/AutoRest' + connection 'scm:git:git://github.com/Azure/AutoRest.git' + } + + licenses { + license { + name 'The MIT License (MIT)' + url 'http://opensource.org/licenses/MIT' + distribution 'repo' + } + } + + developers { + developer { + id 'microsoft' + name 'Microsoft' + } + } + } + } + } +} + +test { + testLogging { + events "passed", "skipped", "failed", "standardError" + } +} + +javadoc { + options.encoding = 'UTF-8' +} + +task sourcesJar(type: Jar, dependsOn:classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: [javadoc]) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar + archives javadocJar +} + +test { + reports.getHtml() + reports.html.destination = file("$rootDir/TestResults/JavaAzureRuntime") +} + +tasks.compileJava.dependsOn 'clean' diff --git a/azure-client-authentication/pom.xml b/azure-client-authentication/pom.xml new file mode 100644 index 0000000000000..47582ca1eaa87 --- /dev/null +++ b/azure-client-authentication/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + com.microsoft.azure + autorest-clientruntime-for-java + 1.0.0-SNAPSHOT + ../pom.xml + + + azure-client-authentication + jar + + Azure Java Client Authentication Library for AutoRest + This package contains the authentication connectors to Active Directory for JDK. + https://github.com/Azure/autorest-clientruntime-for-java + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + + scm:git:https://github.com/Azure/autorest-clientruntime-for-java + scm:git:git@github.com:Azure/autorest-clientruntime-for-java.git + HEAD + + + + UTF-8 + + + + + + microsoft + Microsoft + + + + + + com.microsoft.azure + azure-client-runtime + 1.0.0-SNAPSHOT + + + com.microsoft.azure + adal4j + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.8 + + *.implementation.*;*.utils.*;com.microsoft.schemas._2003._10.serialization;*.blob.core.storage + /** +
* Copyright (c) Microsoft Corporation. All rights reserved. +
* Licensed under the MIT License. See License.txt in the project root for +
* license information. +
*/]]>
+
+
+ +
+
+
diff --git a/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/ApplicationTokenCredentials.java b/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/ApplicationTokenCredentials.java new file mode 100644 index 0000000000000..384ab3f7671af --- /dev/null +++ b/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/ApplicationTokenCredentials.java @@ -0,0 +1,224 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure.credentials; + +import com.microsoft.aad.adal4j.AuthenticationContext; +import com.microsoft.aad.adal4j.AuthenticationResult; +import com.microsoft.aad.adal4j.ClientCredential; +import com.microsoft.azure.AzureEnvironment; +import com.microsoft.rest.credentials.TokenCredentials; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Token based credentials for use with a REST Service Client. + */ +public class ApplicationTokenCredentials extends TokenCredentials { + /** The active directory application client id. */ + private String clientId; + /** The tenant or domain the containing the application. */ + private String domain; + /** The authentication secret for the application. */ + private String secret; + /** The Azure environment to authenticate with. */ + private AzureEnvironment environment; + /** The current authentication result. */ + private AuthenticationResult authenticationResult; + /** The default subscription to use, if any. */ + private String defaultSubscription; + + /** + * Initializes a new instance of the UserTokenCredentials. + * + * @param clientId the active directory application client id. + * @param domain the domain or tenant id containing this application. + * @param secret the authentication secret for the application. + * @param environment the Azure environment to authenticate with. + * If null is provided, AzureEnvironment.AZURE will be used. + */ + public ApplicationTokenCredentials(String clientId, String domain, String secret, AzureEnvironment environment) { + super(null, null); // defer token acquisition + this.clientId = clientId; + this.domain = domain; + this.secret = secret; + if (environment == null) { + this.environment = AzureEnvironment.AZURE; + } else { + this.environment = environment; + } + } + + /** + * Contains the keys of the settings in a Properties file to read credentials from. + */ + private enum CredentialSettings { + /** The subscription GUID. */ + SUBSCRIPTION_ID("subscription"), + /** The tenant GUID or domain. */ + TENANT_ID("tenant"), + /** The client id for the client application. */ + CLIENT_ID("client"), + /** The client secret for the service principal. */ + CLIENT_KEY("key"), + /** The management endpoint. */ + MANAGEMENT_URI("managementURI"), + /** The base URL to the current Azure environment. */ + BASE_URL("baseURL"), + /** The URL to Active Directory authentication. */ + AUTH_URL("authURL"); + + /** The name of the key in the properties file. */ + private final String name; + + CredentialSettings(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + } + + /** + * @return The default subscription ID, if any + */ + public String defaultSubscriptionId() { + return defaultSubscription; + } + + /** + * Set default subscription ID. + * + * @param subscriptionId the default subscription ID. + * @return the credentials object itself. + */ + public ApplicationTokenCredentials withDefaultSubscriptionId(String subscriptionId) { + this.defaultSubscription = subscriptionId; + return this; + } + + /** + * Initializes the credentials based on the provided credentials file. + * + * @param credentialsFile A file with credentials, using the standard Java properties format. + * and the following keys: + * subscription=<subscription-id> + * tenant=<tenant-id> + * client=<client-id> + * key=<client-key> + * managementURI=<management-URI> + * baseURL=<base-URL> + * authURL=<authentication-URL> + * + * @return The credentials based on the file. + * @throws IOException exception thrown from file access errors. + */ + public static ApplicationTokenCredentials fromFile(File credentialsFile) throws IOException { + // Set defaults + Properties authSettings = new Properties(); + authSettings.put(CredentialSettings.AUTH_URL.toString(), AzureEnvironment.AZURE.getAuthenticationEndpoint()); + authSettings.put(CredentialSettings.BASE_URL.toString(), AzureEnvironment.AZURE.getBaseUrl()); + authSettings.put(CredentialSettings.MANAGEMENT_URI.toString(), AzureEnvironment.AZURE.getTokenAudience()); + + // Load the credentials from the file + FileInputStream credentialsFileStream = new FileInputStream(credentialsFile); + authSettings.load(credentialsFileStream); + credentialsFileStream.close(); + + final String clientId = authSettings.getProperty(CredentialSettings.CLIENT_ID.toString()); + final String tenantId = authSettings.getProperty(CredentialSettings.TENANT_ID.toString()); + final String clientKey = authSettings.getProperty(CredentialSettings.CLIENT_KEY.toString()); + final String mgmtUri = authSettings.getProperty(CredentialSettings.MANAGEMENT_URI.toString()); + final String authUrl = authSettings.getProperty(CredentialSettings.AUTH_URL.toString()); + final String baseUrl = authSettings.getProperty(CredentialSettings.BASE_URL.toString()); + final String defaultSubscriptionId = authSettings.getProperty(CredentialSettings.SUBSCRIPTION_ID.toString()); + + return new ApplicationTokenCredentials( + clientId, + tenantId, + clientKey, + new AzureEnvironment( + authUrl, + mgmtUri, + true, + baseUrl) + ).withDefaultSubscriptionId(defaultSubscriptionId); + } + + /** + * Gets the active directory application client id. + * + * @return the active directory application client id. + */ + public String getClientId() { + return clientId; + } + + /** + * Gets the tenant or domain the containing the application. + * + * @return the tenant or domain the containing the application. + */ + public String getDomain() { + return domain; + } + + /** + * Gets the authentication secret for the application. + * + * @return the authentication secret for the application. + */ + public String getSecret() { + return secret; + } + + /** + * Gets the Azure environment to authenticate with. + * + * @return the Azure environment to authenticate with. + */ + public AzureEnvironment getEnvironment() { + return environment; + } + + @Override + public String getToken() throws IOException { + if (authenticationResult == null + || authenticationResult.getAccessToken() == null) { + acquireAccessToken(); + } + return authenticationResult.getAccessToken(); + } + + @Override + public void refreshToken() throws IOException { + acquireAccessToken(); + } + + private void acquireAccessToken() throws IOException { + String authorityUrl = this.getEnvironment().getAuthenticationEndpoint() + this.getDomain(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + AuthenticationContext context = new AuthenticationContext(authorityUrl, this.getEnvironment().isValidateAuthority(), executor); + try { + authenticationResult = context.acquireToken( + this.getEnvironment().getTokenAudience(), + new ClientCredential(this.getClientId(), this.getSecret()), + null).get(); + } catch (Exception e) { + throw new IOException(e.getMessage(), e); + } finally { + executor.shutdown(); + } + } +} diff --git a/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/UserTokenCredentials.java b/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/UserTokenCredentials.java new file mode 100644 index 0000000000000..60d6bdb3c0a58 --- /dev/null +++ b/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/UserTokenCredentials.java @@ -0,0 +1,164 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure.credentials; + +import com.microsoft.aad.adal4j.AuthenticationContext; +import com.microsoft.aad.adal4j.AuthenticationResult; +import com.microsoft.azure.AzureEnvironment; +import com.microsoft.rest.credentials.TokenCredentials; + +import java.io.IOException; +import java.util.Date; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Token based credentials for use with a REST Service Client. + */ +public class UserTokenCredentials extends TokenCredentials { + /** The Active Directory application client id. */ + private String clientId; + /** The domain or tenant id containing this application. */ + private String domain; + /** The user name for the Organization Id account. */ + private String username; + /** The password for the Organization Id account. */ + private String password; + /** The Uri where the user will be redirected after authenticating with AD. */ + private String clientRedirectUri; + /** The Azure environment to authenticate with. */ + private AzureEnvironment environment; + /** The current authentication result. */ + private AuthenticationResult authenticationResult; + + /** + * Initializes a new instance of the UserTokenCredentials. + * + * @param clientId the active directory application client id. + * @param domain the domain or tenant id containing this application. + * @param username the user name for the Organization Id account. + * @param password the password for the Organization Id account. + * @param clientRedirectUri the Uri where the user will be redirected after authenticating with AD. + * @param environment the Azure environment to authenticate with. + * If null is provided, AzureEnvironment.AZURE will be used. + */ + public UserTokenCredentials(String clientId, String domain, String username, String password, String clientRedirectUri, AzureEnvironment environment) { + super(null, null); // defer token acquisition + this.clientId = clientId; + this.domain = domain; + this.username = username; + this.password = password; + this.clientRedirectUri = clientRedirectUri; + if (environment == null) { + this.environment = AzureEnvironment.AZURE; + } else { + this.environment = environment; + } + } + + /** + * Gets the active directory application client id. + * + * @return the active directory application client id. + */ + public String getClientId() { + return clientId; + } + + /** + * Gets the tenant or domain the containing the application. + * + * @return the tenant or domain the containing the application. + */ + public String getDomain() { + return domain; + } + + /** + * Gets the user name for the Organization Id account. + * + * @return the user name. + */ + public String getUsername() { + return username; + } + + /** + * Gets the password for the Organization Id account. + * + * @return the password. + */ + public String getPassword() { + return password; + } + + /** + * Gets the Uri where the user will be redirected after authenticating with AD. + * + * @return the redirecting Uri. + */ + public String getClientRedirectUri() { + return clientRedirectUri; + } + + /** + * Gets the Azure environment to authenticate with. + * + * @return the Azure environment to authenticate with. + */ + public AzureEnvironment getEnvironment() { + return environment; + } + + @Override + public String getToken() throws IOException { + if (authenticationResult != null + && authenticationResult.getExpiresOnDate().before(new Date())) { + acquireAccessTokenFromRefreshToken(); + } else { + acquireAccessToken(); + } + return authenticationResult.getAccessToken(); + } + + @Override + public void refreshToken() throws IOException { + acquireAccessToken(); + } + + private void acquireAccessToken() throws IOException { + String authorityUrl = this.getEnvironment().getAuthenticationEndpoint() + this.getDomain(); + AuthenticationContext context = new AuthenticationContext(authorityUrl, this.getEnvironment().isValidateAuthority(), Executors.newSingleThreadExecutor()); + try { + authenticationResult = context.acquireToken( + this.getEnvironment().getTokenAudience(), + this.getClientId(), + this.getUsername(), + this.getPassword(), + null).get(); + } catch (Exception e) { + throw new IOException(e.getMessage(), e); + } + } + + private void acquireAccessTokenFromRefreshToken() throws IOException { + String authorityUrl = this.getEnvironment().getAuthenticationEndpoint() + this.getDomain(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + AuthenticationContext context = new AuthenticationContext(authorityUrl, this.getEnvironment().isValidateAuthority(), executor); + try { + authenticationResult = context.acquireTokenByRefreshToken( + authenticationResult.getRefreshToken(), + this.getClientId(), + null, null).get(); + } catch (Exception e) { + throw new IOException(e.getMessage(), e); + } finally { + executor.shutdown(); + } + } +} diff --git a/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/package-info.java b/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/package-info.java new file mode 100644 index 0000000000000..2f23671d5be16 --- /dev/null +++ b/azure-client-authentication/src/main/java/com/microsoft/azure/credentials/package-info.java @@ -0,0 +1,5 @@ +/** + * The package provides 2 credential classes that work with AutoRest + * generated Azure clients for authentication purposes through Azure. + */ +package com.microsoft.azure.credentials; \ No newline at end of file diff --git a/azure-client-authentication/src/test/java/com/microsoft/azure/credentials/UserTokenCredentialsTests.java b/azure-client-authentication/src/test/java/com/microsoft/azure/credentials/UserTokenCredentialsTests.java new file mode 100644 index 0000000000000..7905ba7e7b21e --- /dev/null +++ b/azure-client-authentication/src/test/java/com/microsoft/azure/credentials/UserTokenCredentialsTests.java @@ -0,0 +1,81 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure.credentials; + +import com.microsoft.aad.adal4j.AuthenticationResult; +import com.microsoft.azure.AzureEnvironment; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.util.Date; + +public class UserTokenCredentialsTests { + private static MockUserTokenCredentials credentials = new MockUserTokenCredentials( + "clientId", + "domain", + "username", + "password", + "redirect", + AzureEnvironment.AZURE + ); + + @Test + public void testAcquireToken() throws Exception { + credentials.refreshToken(); + Assert.assertEquals("token1", credentials.getToken()); + Thread.sleep(1500); + Assert.assertEquals("token2", credentials.getToken()); + } + + public static class MockUserTokenCredentials extends UserTokenCredentials { + private AuthenticationResult authenticationResult; + + public MockUserTokenCredentials(String clientId, String domain, String username, String password, String clientRedirectUri, AzureEnvironment environment) { + super(clientId, domain, username, password, clientRedirectUri, environment); + } + + @Override + public String getToken() throws IOException { + if (authenticationResult != null + && authenticationResult.getExpiresOnDate().before(new Date())) { + acquireAccessTokenFromRefreshToken(); + } else { + acquireAccessToken(); + } + return authenticationResult.getAccessToken(); + } + + @Override + public void refreshToken() throws IOException { + acquireAccessToken(); + } + + private void acquireAccessToken() throws IOException { + this.authenticationResult = new AuthenticationResult( + null, + "token1", + "refresh", + 1, + null, + null, + false); + } + + private void acquireAccessTokenFromRefreshToken() throws IOException { + this.authenticationResult = new AuthenticationResult( + null, + "token2", + "refresh", + 1, + null, + null, + false); + } + } +} diff --git a/azure-client-runtime/build.gradle b/azure-client-runtime/build.gradle new file mode 100644 index 0000000000000..db64fd0b46abd --- /dev/null +++ b/azure-client-runtime/build.gradle @@ -0,0 +1,96 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.2' + } +} + +apply plugin: 'java' +apply plugin: 'checkstyle' + +version = '1.0.0-SNAPSHOT' + +checkstyle { + toolVersion = "6.18" + configFile = new File("$rootDir/src/client/Java/build-tools/src/main/resources/checkstyle.xml") + configProperties = [samedir: "$rootDir/src/client/Java/build-tools/src/main/resources"] + reportsDir = new File("$rootDir/src/client/Java/build-tools/reports") +} + +dependencies { + compile 'com.microsoft.rest:client-runtime:1.0.0-SNAPSHOT' + testCompile 'junit:junit:4.12' + testCompile 'junit:junit-dep:4.11' + deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" +} + +uploadArchives { + repositories { + mavenDeployer { + configuration = configurations.deployerJars + snapshotRepository(url: "ftp://waws-prod-bay-005.ftp.azurewebsites.windows.net/site/wwwroot/") { + authentication(userName: username, password: password) + } + pom.setArtifactId "azure-client-runtime" + pom.project { + name 'Microsoft Azure AutoRest Runtime for Java' + description 'This is the client runtime for AutoRest generated Azure Java clients.' + url 'https://github.com/Azure/autorest' + + scm { + url 'scm:git:https://github.com/Azure/AutoRest' + connection 'scm:git:git://github.com/Azure/AutoRest.git' + } + + licenses { + license { + name 'The MIT License (MIT)' + url 'http://opensource.org/licenses/MIT' + distribution 'repo' + } + } + + developers { + developer { + id 'microsoft' + name 'Microsoft' + } + } + } + } + } +} + +test { + testLogging { + events "passed", "skipped", "failed", "standardError" + } +} + +javadoc { + options.encoding = 'UTF-8' +} + +task sourcesJar(type: Jar, dependsOn:classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: [javadoc]) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar + archives javadocJar +} + +test { + reports.getHtml() + reports.html.destination = file("$rootDir/TestResults/JavaAzureRuntime") +} + +tasks.compileJava.dependsOn 'clean' diff --git a/azure-client-runtime/pom.xml b/azure-client-runtime/pom.xml new file mode 100644 index 0000000000000..98d91b43e802e --- /dev/null +++ b/azure-client-runtime/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + com.microsoft.azure + autorest-clientruntime-for-java + 1.0.0-SNAPSHOT + ../pom.xml + + + azure-client-runtime + jar + + Azure Java Client Runtime for AutoRest + This package contains the basic runtime for AutoRest generated Azure Java clients. + https://github.com/Azure/autorest-clientruntime-for-java + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + + scm:git:https://github.com/Azure/autorest-clientruntime-for-java + scm:git:git@github.com:Azure/autorest-clientruntime-for-java.git + HEAD + + + + UTF-8 + + + + + + microsoft + Microsoft + + + + + + com.microsoft.rest + client-runtime + ${parent.version} + + + junit + junit + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.8 + + *.implementation.*;*.utils.*;com.microsoft.schemas._2003._10.serialization;*.blob.core.storage + /** +
* Copyright (c) Microsoft Corporation. All rights reserved. +
* Licensed under the MIT License. See License.txt in the project root for +
* license information. +
*/]]>
+
+
+ +
+
+
diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureAsyncOperation.java b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureAsyncOperation.java new file mode 100644 index 0000000000000..18f56454e9144 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureAsyncOperation.java @@ -0,0 +1,139 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import java.util.Arrays; +import java.util.List; + +/** + * The response body contains the status of the specified + * asynchronous operation, indicating whether it has succeeded, is in + * progress, or has failed. Note that this status is distinct from the + * HTTP status code returned for the Get Operation Status operation + * itself. If the asynchronous operation succeeded, the response body + * includes the HTTP status code for the successful request. If the + * asynchronous operation failed, the response body includes the HTTP + * status code for the failed request, and also includes error + * information regarding the failure. + */ +public class AzureAsyncOperation { + /** + * Default delay in seconds for long running operations. + */ + static final int DEFAULT_DELAY = 30; + + /** + * Successful status for long running operations. + */ + static final String SUCCESS_STATUS = "Succeeded"; + + /** + * In progress status for long running operations. + */ + static final String IN_PROGRESS_STATUS = "InProgress"; + + /** + * Failed status for long running operations. + */ + static final String FAILED_STATUS = "Failed"; + + /** + * Canceled status for long running operations. + */ + static final String CANCELED_STATUS = "Canceled"; + + /** + * Gets failed terminal statuses for long running operations. + * + * @return a list of statuses indicating a failed operation. + */ + public static List getFailedStatuses() { + return Arrays.asList(FAILED_STATUS, CANCELED_STATUS); + } + + /** + * Gets terminal statuses for long running operations. + * + * @return a list of terminal statuses. + */ + public static List getTerminalStatuses() { + return Arrays.asList(FAILED_STATUS, CANCELED_STATUS, SUCCESS_STATUS); + } + + /** + * The status of the asynchronous request. + */ + private String status; + + /** + * Gets the status of the asynchronous request. + * + * @return the status of the asynchronous request. + */ + public String getStatus() { + return this.status; + } + + /** + * Sets the status of the asynchronous request. + * + * @param status the status of the asynchronous request. + */ + public void setStatus(String status) { + this.status = status; + } + + /** + * If the asynchronous operation failed, the response body includes + * the HTTP status code for the failed request, and also includes + * error information regarding the failure. + */ + private CloudError error; + + /** + * Gets the cloud error. + * + * @return the cloud error. + */ + public CloudError getError() { + return this.error; + } + + /** + * Sets the cloud error. + * + * @param error the cloud error. + */ + public void setError(CloudError error) { + this.error = error; + } + + /** + * The delay in seconds that should be used when checking + * for the status of the operation. + */ + private int retryAfter; + + /** + * Gets the delay in seconds. + * + * @return the delay in seconds. + */ + public int getRetryAfter() { + return this.retryAfter; + } + + /** + * Sets the delay in seconds. + * + * @param retryAfter the delay in seconds. + */ + public void setRetryAfter(int retryAfter) { + this.retryAfter = retryAfter; + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureClient.java b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureClient.java new file mode 100644 index 0000000000000..62a760acb4a0d --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureClient.java @@ -0,0 +1,876 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.ServiceCall; +import com.microsoft.rest.ServiceCallback; +import com.microsoft.rest.ServiceException; +import com.microsoft.rest.ServiceResponse; +import com.microsoft.rest.ServiceResponseCallback; +import com.microsoft.rest.ServiceResponseWithHeaders; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Url; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * An instance of this class defines a ServiceClient that handles polling and + * retrying for long running operations when accessing Azure resources. + */ +public class AzureClient extends AzureServiceClient { + /** + * The interval time between two long running operation polls. Default is + * used if null. + */ + private Integer longRunningOperationRetryTimeout; + /** + * The executor for asynchronous requests. + */ + private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + + /** + * The user agent from the service client that owns this Azure Client. + */ + private final String serviceClientUserAgent; + + /** + * Initializes an instance of this class with customized client metadata. + * + * @param serviceClient the caller client that initiates the asynchronous request. + */ + public AzureClient(AzureServiceClient serviceClient) { + super(serviceClient.restClient()); + this.serviceClientUserAgent = serviceClient.userAgent(); + } + + /** + * Handles an initial response from a PUT or PATCH operation response by polling + * the status of the operation until the long running operation terminates. + * + * @param response the initial response from the PUT or PATCH operation. + * @param the return type of the caller + * @param resourceType the type of the resource + * @return the terminal response for the operation. + * @throws CloudException REST exception + * @throws InterruptedException interrupted exception + * @throws IOException thrown by deserialization + */ + public ServiceResponse getPutOrPatchResult(Response response, Type resourceType) throws CloudException, InterruptedException, IOException { + if (response == null) { + throw new CloudException("response is null."); + } + + int statusCode = response.code(); + ResponseBody responseBody; + if (response.isSuccessful()) { + responseBody = response.body(); + } else { + responseBody = response.errorBody(); + } + if (statusCode != 200 && statusCode != 201 && statusCode != 202) { + CloudException exception = new CloudException(statusCode + " is not a valid polling status code"); + exception.setResponse(response); + if (responseBody != null) { + exception.setBody((CloudError) restClient().mapperAdapter().deserialize(responseBody.string(), CloudError.class)); + responseBody.close(); + } + throw exception; + } + + PollingState pollingState = new PollingState<>(response, this.getLongRunningOperationRetryTimeout(), resourceType, restClient().mapperAdapter()); + String url = response.raw().request().url().toString(); + + // Check provisioning state + while (!AzureAsyncOperation.getTerminalStatuses().contains(pollingState.getStatus())) { + Thread.sleep(pollingState.getDelayInMilliseconds()); + + if (pollingState.getAzureAsyncOperationHeaderLink() != null + && !pollingState.getAzureAsyncOperationHeaderLink().isEmpty()) { + updateStateFromAzureAsyncOperationHeader(pollingState); + } else if (pollingState.getLocationHeaderLink() != null + && !pollingState.getLocationHeaderLink().isEmpty()) { + updateStateFromLocationHeaderOnPut(pollingState); + } else { + updateStateFromGetResourceOperation(pollingState, url); + } + } + + if (AzureAsyncOperation.SUCCESS_STATUS.equals(pollingState.getStatus()) && pollingState.getResource() == null) { + updateStateFromGetResourceOperation(pollingState, url); + } + + if (AzureAsyncOperation.getFailedStatuses().contains(pollingState.getStatus())) { + throw new CloudException("Async operation failed"); + } + + return new ServiceResponse<>(pollingState.getResource(), pollingState.getResponse()); + } + + /** + * Handles an initial response from a PUT or PATCH operation response by polling + * the status of the operation until the long running operation terminates. + * + * @param response the initial response from the PUT or PATCH operation. + * @param resourceType the type of the resource + * @param headerType the type of the response header + * @param the return type of the caller + * @param the type of the response header + * @return the terminal response for the operation. + * @throws CloudException REST exception + * @throws InterruptedException interrupted exception + * @throws IOException thrown by deserialization + */ + public ServiceResponseWithHeaders getPutOrPatchResultWithHeaders(Response response, Type resourceType, Class headerType) throws CloudException, InterruptedException, IOException { + ServiceResponse bodyResponse = getPutOrPatchResult(response, resourceType); + return new ServiceResponseWithHeaders<>( + bodyResponse.getBody(), + restClient().mapperAdapter().deserialize(restClient().mapperAdapter().serialize(bodyResponse.getResponse().headers()), headerType), + bodyResponse.getResponse() + ); + } + + /** + * Handles an initial response from a PUT or PATCH operation response by polling + * the status of the operation asynchronously, calling the user provided callback + * when the operation terminates. + * + * @param response the initial response from the PUT or PATCH operation. + * @param the return type of the caller. + * @param resourceType the type of the resource. + * @param serviceCall the ServiceCall object tracking Retrofit calls. + * @param callback the user callback to call when operation terminates. + * @return the task describing the asynchronous polling. + */ + public AsyncPollingTask getPutOrPatchResultAsync(Response response, Type resourceType, ServiceCall serviceCall, ServiceCallback callback) { + if (response == null) { + callback.failure(new ServiceException("response is null.")); + return null; + } + + int statusCode = response.code(); + ResponseBody responseBody; + if (response.isSuccessful()) { + responseBody = response.body(); + } else { + responseBody = response.errorBody(); + } + if (statusCode != 200 && statusCode != 201 && statusCode != 202) { + CloudException exception = new CloudException(statusCode + " is not a valid polling status code"); + exception.setResponse(response); + try { + if (responseBody != null) { + exception.setBody((CloudError) restClient().mapperAdapter().deserialize(responseBody.string(), CloudError.class)); + responseBody.close(); + } + } catch (Exception e) { /* ignore serialization errors on top of service errors */ } + callback.failure(exception); + return null; + } + + PollingState pollingState; + try { + pollingState = new PollingState<>(response, this.getLongRunningOperationRetryTimeout(), resourceType, restClient().mapperAdapter()); + } catch (IOException e) { + callback.failure(e); + return null; + } + String url = response.raw().request().url().toString(); + + // Task runner will take it from here + PutPatchPollingTask task = new PutPatchPollingTask<>(pollingState, url, serviceCall, callback); + executor.schedule(task, pollingState.getDelayInMilliseconds(), TimeUnit.MILLISECONDS); + return task; + } + + /** + * Handles an initial response from a PUT or PATCH operation response by polling + * the status of the operation asynchronously, calling the user provided callback + * when the operation terminates. + * + * @param response the initial response from the PUT or PATCH operation. + * @param the return type of the caller + * @param the type of the response header + * @param resourceType the type of the resource. + * @param headerType the type of the response header + * @param serviceCall the ServiceCall object tracking Retrofit calls. + * @param callback the user callback to call when operation terminates. + * @return the task describing the asynchronous polling. + */ + public AsyncPollingTask getPutOrPatchResultWithHeadersAsync(Response response, Type resourceType, final Class headerType, final ServiceCall serviceCall, final ServiceCallback callback) { + return this.getPutOrPatchResultAsync(response, resourceType, serviceCall, new ServiceCallback() { + @Override + public void failure(Throwable t) { + callback.failure(t); + } + + @Override + public void success(ServiceResponse result) { + try { + callback.success(new ServiceResponseWithHeaders<>( + result.getBody(), + restClient().mapperAdapter().deserialize(restClient().mapperAdapter().serialize(result.getResponse().headers()), headerType), + result.getResponse() + )); + } catch (IOException e) { + failure(e); + } + } + }); + } + + /** + * Handles an initial response from a POST or DELETE operation response by polling + * the status of the operation until the long running operation terminates. + * + * @param response the initial response from the POST or DELETE operation. + * @param the return type of the caller + * @param resourceType the type of the resource + * @return the terminal response for the operation. + * @throws CloudException REST exception + * @throws InterruptedException interrupted exception + * @throws IOException thrown by deserialization + */ + public ServiceResponse getPostOrDeleteResult(Response response, Type resourceType) throws CloudException, InterruptedException, IOException { + if (response == null) { + throw new CloudException("response is null."); + } + + int statusCode = response.code(); + ResponseBody responseBody; + if (response.isSuccessful()) { + responseBody = response.body(); + } else { + responseBody = response.errorBody(); + } + if (statusCode != 200 && statusCode != 202 && statusCode != 204) { + CloudException exception = new CloudException(statusCode + " is not a valid polling status code"); + exception.setResponse(response); + if (responseBody != null) { + exception.setBody((CloudError) restClient().mapperAdapter().deserialize(responseBody.string(), CloudError.class)); + responseBody.close(); + } + throw exception; + } + + PollingState pollingState = new PollingState<>(response, this.getLongRunningOperationRetryTimeout(), resourceType, restClient().mapperAdapter()); + + // Check provisioning state + while (!AzureAsyncOperation.getTerminalStatuses().contains(pollingState.getStatus())) { + Thread.sleep(pollingState.getDelayInMilliseconds()); + + if (pollingState.getAzureAsyncOperationHeaderLink() != null + && !pollingState.getAzureAsyncOperationHeaderLink().isEmpty()) { + updateStateFromAzureAsyncOperationHeader(pollingState); + } else if (pollingState.getLocationHeaderLink() != null + && !pollingState.getLocationHeaderLink().isEmpty()) { + updateStateFromLocationHeaderOnPostOrDelete(pollingState); + } else { + CloudException exception = new CloudException("No header in response"); + exception.setResponse(response); + throw exception; + } + } + + // Check if operation failed + if (AzureAsyncOperation.getFailedStatuses().contains(pollingState.getStatus())) { + throw new CloudException("Async operation failed"); + } + + return new ServiceResponse<>(pollingState.getResource(), pollingState.getResponse()); + } + + /** + * Handles an initial response from a POST or DELETE operation response by polling + * the status of the operation until the long running operation terminates. + * + * @param response the initial response from the POST or DELETE operation. + * @param resourceType the type of the resource + * @param headerType the type of the response header + * @param the return type of the caller + * @param the type of the response header + * @return the terminal response for the operation. + * @throws CloudException REST exception + * @throws InterruptedException interrupted exception + * @throws IOException thrown by deserialization + */ + public ServiceResponseWithHeaders getPostOrDeleteResultWithHeaders(Response response, Type resourceType, Class headerType) throws CloudException, InterruptedException, IOException { + ServiceResponse bodyResponse = getPostOrDeleteResult(response, resourceType); + return new ServiceResponseWithHeaders<>( + bodyResponse.getBody(), + restClient().mapperAdapter().deserialize(restClient().mapperAdapter().serialize(bodyResponse.getResponse().headers()), headerType), + bodyResponse.getResponse() + ); + } + + /** + * Handles an initial response from a POST or DELETE operation response by polling + * the status of the operation asynchronously, calling the user provided callback + * when the operation terminates. + * + * @param response the initial response from the POST or DELETE operation. + * @param the return type of the caller. + * @param resourceType the type of the resource. + * @param serviceCall the ServiceCall object tracking Retrofit calls. + * @param callback the user callback to call when operation terminates. + * @return the task describing the asynchronous polling. + */ + public AsyncPollingTask getPostOrDeleteResultAsync(Response response, Type resourceType, ServiceCall serviceCall, ServiceCallback callback) { + if (response == null) { + callback.failure(new ServiceException("response is null.")); + return null; + } + + int statusCode = response.code(); + ResponseBody responseBody; + if (response.isSuccessful()) { + responseBody = response.body(); + } else { + responseBody = response.errorBody(); + } + if (statusCode != 200 && statusCode != 202 && statusCode != 204) { + CloudException exception = new CloudException(statusCode + " is not a valid polling status code"); + exception.setResponse(response); + try { + if (responseBody != null) { + exception.setBody((CloudError) restClient().mapperAdapter().deserialize(responseBody.string(), CloudError.class)); + responseBody.close(); + } + } catch (Exception e) { /* ignore serialization errors on top of service errors */ } + callback.failure(exception); + return null; + } + + PollingState pollingState; + try { + pollingState = new PollingState<>(response, this.getLongRunningOperationRetryTimeout(), resourceType, restClient().mapperAdapter()); + } catch (IOException e) { + callback.failure(e); + return null; + } + + // Task runner will take it from here + PostDeletePollingTask task = new PostDeletePollingTask<>(pollingState, serviceCall, callback); + executor.schedule(task, pollingState.getDelayInMilliseconds(), TimeUnit.MILLISECONDS); + return task; + } + + /** + * Handles an initial response from a POST or DELETE operation response by polling + * the status of the operation asynchronously, calling the user provided callback + * when the operation terminates. + * + * @param response the initial response from the POST or DELETE operation. + * @param the return type of the caller + * @param the type of the response header + * @param resourceType the type of the resource. + * @param headerType the type of the response header + * @param serviceCall the ServiceCall object tracking Retrofit calls. + * @param callback the user callback to call when operation terminates. + * @return the task describing the asynchronous polling. + */ + public AsyncPollingTask getPostOrDeleteResultWithHeadersAsync(Response response, Type resourceType, final Class headerType, final ServiceCall serviceCall, final ServiceCallback callback) { + return this.getPostOrDeleteResultAsync(response, resourceType, serviceCall, new ServiceCallback() { + @Override + public void failure(Throwable t) { + callback.failure(t); + } + + @Override + public void success(ServiceResponse result) { + try { + callback.success(new ServiceResponseWithHeaders<>( + result.getBody(), + restClient().mapperAdapter().deserialize(restClient().mapperAdapter().serialize(result.getResponse().headers()), headerType), + result.getResponse() + )); + } catch (IOException e) { + failure(e); + } + } + }); + } + + /** + * Polls from the location header and updates the polling state with the + * polling response for a PUT operation. + * + * @param pollingState the polling state for the current operation. + * @param the return type of the caller. + * @throws CloudException REST exception + * @throws IOException thrown by deserialization + */ + private void updateStateFromLocationHeaderOnPut(PollingState pollingState) throws CloudException, IOException { + Response response = poll(pollingState.getLocationHeaderLink()); + int statusCode = response.code(); + if (statusCode == 202) { + pollingState.setResponse(response); + pollingState.setStatus(AzureAsyncOperation.IN_PROGRESS_STATUS); + } else if (statusCode == 200 || statusCode == 201) { + pollingState.updateFromResponseOnPutPatch(response); + } + } + + /** + * Polls from the location header and updates the polling state with the + * polling response for a PUT operation. + * + * @param pollingState the polling state for the current operation. + * @param callback the user callback to call when operation terminates. + * @param the return type of the caller. + * @return the task describing the asynchronous polling. + */ + private Call updateStateFromLocationHeaderOnPutAsync(final PollingState pollingState, final ServiceCallback callback) { + return pollAsync(pollingState.getLocationHeaderLink(), new ServiceCallback() { + @Override + public void failure(Throwable t) { + callback.failure(t); + } + + @Override + public void success(ServiceResponse result) { + try { + int statusCode = result.getResponse().code(); + if (statusCode == 202) { + pollingState.setResponse(result.getResponse()); + pollingState.setStatus(AzureAsyncOperation.IN_PROGRESS_STATUS); + } else if (statusCode == 200 || statusCode == 201) { + pollingState.updateFromResponseOnPutPatch(result.getResponse()); + } + callback.success(new ServiceResponse<>(pollingState.getResource(), pollingState.getResponse())); + } catch (Throwable t) { + failure(t); + } + } + }); + } + + /** + * Polls from the location header and updates the polling state with the + * polling response for a POST or DELETE operation. + * + * @param pollingState the polling state for the current operation. + * @param the return type of the caller. + * @throws CloudException service exception + * @throws IOException thrown by deserialization + */ + private void updateStateFromLocationHeaderOnPostOrDelete(PollingState pollingState) throws CloudException, IOException { + Response response = poll(pollingState.getLocationHeaderLink()); + int statusCode = response.code(); + if (statusCode == 202) { + pollingState.setResponse(response); + pollingState.setStatus(AzureAsyncOperation.IN_PROGRESS_STATUS); + } else if (statusCode == 200 || statusCode == 201 || statusCode == 204) { + pollingState.updateFromResponseOnDeletePost(response); + } + } + + /** + * Polls from the location header and updates the polling state with the + * polling response for a POST or DELETE operation. + * + * @param pollingState the polling state for the current operation. + * @param callback the user callback to call when operation terminates. + * @param the return type of the caller. + * @return the task describing the asynchronous polling. + */ + private Call updateStateFromLocationHeaderOnPostOrDeleteAsync(final PollingState pollingState, final ServiceCallback callback) { + return pollAsync(pollingState.getLocationHeaderLink(), new ServiceCallback() { + @Override + public void failure(Throwable t) { + callback.failure(t); + } + + @Override + public void success(ServiceResponse result) { + try { + int statusCode = result.getResponse().code(); + if (statusCode == 202) { + pollingState.setResponse(result.getResponse()); + pollingState.setStatus(AzureAsyncOperation.IN_PROGRESS_STATUS); + } else if (statusCode == 200 || statusCode == 201 || statusCode == 204) { + pollingState.updateFromResponseOnDeletePost(result.getResponse()); + } + callback.success(new ServiceResponse<>(pollingState.getResource(), pollingState.getResponse())); + } catch (Throwable t) { + failure(t); + } + } + }); + } + + /** + * Polls from the provided URL and updates the polling state with the + * polling response. + * + * @param pollingState the polling state for the current operation. + * @param url the url to poll from + * @param the return type of the caller. + * @throws CloudException service exception + * @throws IOException thrown by deserialization + */ + private void updateStateFromGetResourceOperation(PollingState pollingState, String url) throws CloudException, IOException { + Response response = poll(url); + pollingState.updateFromResponseOnPutPatch(response); + } + + /** + * Polls from the provided URL and updates the polling state with the + * polling response. + * + * @param pollingState the polling state for the current operation. + * @param url the url to poll from + * @param callback the user callback to call when operation terminates. + * @param the return type of the caller. + * @return the task describing the asynchronous polling. + */ + private Call updateStateFromGetResourceOperationAsync(final PollingState pollingState, String url, final ServiceCallback callback) { + return pollAsync(url, new ServiceCallback() { + @Override + public void failure(Throwable t) { + callback.failure(t); + } + + @Override + public void success(ServiceResponse result) { + try { + pollingState.updateFromResponseOnPutPatch(result.getResponse()); + callback.success(new ServiceResponse<>(pollingState.getResource(), pollingState.getResponse())); + } catch (Throwable t) { + failure(t); + } + } + }); + } + + /** + * Polls from the 'Azure-AsyncOperation' header and updates the polling + * state with the polling response. + * + * @param pollingState the polling state for the current operation. + * @param the return type of the caller. + * @throws CloudException service exception + * @throws IOException thrown by deserialization + */ + private void updateStateFromAzureAsyncOperationHeader(PollingState pollingState) throws CloudException, IOException { + Response response = poll(pollingState.getAzureAsyncOperationHeaderLink()); + + AzureAsyncOperation body = null; + if (response.body() != null) { + body = restClient().mapperAdapter().deserialize(response.body().string(), AzureAsyncOperation.class); + response.body().close(); + } + + if (body == null || body.getStatus() == null) { + CloudException exception = new CloudException("no body"); + exception.setResponse(response); + if (response.errorBody() != null) { + exception.setBody((CloudError) restClient().mapperAdapter().deserialize(response.errorBody().string(), CloudError.class)); + response.errorBody().close(); + } + throw exception; + } + + pollingState.setStatus(body.getStatus()); + pollingState.setResponse(response); + pollingState.setResource(null); + } + + /** + * Polls from the 'Azure-AsyncOperation' header and updates the polling + * state with the polling response. + * + * @param pollingState the polling state for the current operation. + * @param callback the user callback to call when operation terminates. + * @param the return type of the caller. + * @return the task describing the asynchronous polling. + */ + private Call updateStateFromAzureAsyncOperationHeaderAsync(final PollingState pollingState, final ServiceCallback callback) { + return pollAsync(pollingState.getAzureAsyncOperationHeaderLink(), new ServiceCallback() { + @Override + public void failure(Throwable t) { + callback.failure(t); + } + + @Override + public void success(ServiceResponse result) { + try { + AzureAsyncOperation body = null; + if (result.getBody() != null) { + body = restClient().mapperAdapter().deserialize(result.getBody().string(), AzureAsyncOperation.class); + result.getBody().close(); + } + if (body == null || body.getStatus() == null) { + CloudException exception = new CloudException("no body"); + exception.setResponse(result.getResponse()); + if (result.getResponse().errorBody() != null) { + exception.setBody((CloudError) restClient().mapperAdapter().deserialize(result.getResponse().errorBody().string(), CloudError.class)); + result.getResponse().errorBody().close(); + } + failure(exception); + } else { + pollingState.setStatus(body.getStatus()); + pollingState.setResponse(result.getResponse()); + pollingState.setResource(null); + callback.success(new ServiceResponse<>(pollingState.getResource(), pollingState.getResponse())); + } + } catch (IOException ex) { + failure(ex); + } + } + }); + } + + /** + * Polls from the URL provided. + * + * @param url the URL to poll from. + * @return the raw response. + * @throws CloudException REST exception + * @throws IOException thrown by deserialization + */ + private Response poll(String url) throws CloudException, IOException { + URL endpoint; + endpoint = new URL(url); + int port = endpoint.getPort(); + if (port == -1) { + port = endpoint.getDefaultPort(); + } + AsyncService service = restClient().retrofit().create(AsyncService.class); + Response response = service.get(endpoint.getFile(), serviceClientUserAgent).execute(); + int statusCode = response.code(); + if (statusCode != 200 && statusCode != 201 && statusCode != 202 && statusCode != 204) { + CloudException exception = new CloudException(statusCode + " is not a valid polling status code"); + exception.setResponse(response); + if (response.body() != null) { + exception.setBody((CloudError) restClient().mapperAdapter().deserialize(response.body().string(), CloudError.class)); + response.body().close(); + } else if (response.errorBody() != null) { + exception.setBody((CloudError) restClient().mapperAdapter().deserialize(response.errorBody().string(), CloudError.class)); + response.errorBody().close(); + } + throw exception; + } + return response; + } + + /** + * Polls asynchronously from the URL provided. + * + * @param url the URL to poll from. + * @param callback the user callback to call when operation terminates. + * @return the {@link Call} object from Retrofit. + */ + private Call pollAsync(String url, final ServiceCallback callback) { + URL endpoint; + try { + endpoint = new URL(url); + } catch (MalformedURLException e) { + callback.failure(e); + return null; + } + int port = endpoint.getPort(); + if (port == -1) { + port = endpoint.getDefaultPort(); + } + AsyncService service = restClient().retrofit().create(AsyncService.class); + Call call = service.get(endpoint.getFile(), serviceClientUserAgent); + call.enqueue(new ServiceResponseCallback(callback) { + @Override + public void onResponse(Call call, Response response) { + try { + int statusCode = response.code(); + if (statusCode != 200 && statusCode != 201 && statusCode != 202 && statusCode != 204) { + CloudException exception = new CloudException(statusCode + " is not a valid polling status code"); + exception.setResponse(response); + if (response.body() != null) { + exception.setBody((CloudError) restClient().mapperAdapter().deserialize(response.body().string(), CloudError.class)); + response.body().close(); + } else if (response.errorBody() != null) { + exception.setBody((CloudError) restClient().mapperAdapter().deserialize(response.errorBody().string(), CloudError.class)); + response.errorBody().close(); + } + callback.failure(exception); + return; + } + callback.success(new ServiceResponse<>(response.body(), response)); + } catch (IOException ex) { + callback.failure(ex); + } + } + }); + return call; + } + + /** + * Gets the interval time between two long running operation polls. + * + * @return the time in milliseconds. + */ + public Integer getLongRunningOperationRetryTimeout() { + return longRunningOperationRetryTimeout; + } + + /** + * Sets the interval time between two long running operation polls. + * + * @param longRunningOperationRetryTimeout the time in milliseconds. + */ + public void withLongRunningOperationRetryTimeout(Integer longRunningOperationRetryTimeout) { + this.longRunningOperationRetryTimeout = longRunningOperationRetryTimeout; + } + + /** + * The Retrofit service used for polling. + */ + private interface AsyncService { + @GET + Call get(@Url String url, @Header("User-Agent") String userAgent); + } + + /** + * The task runner that describes the state of an asynchronous long running + * operation. + * + * @param the return type of the caller. + */ + abstract class AsyncPollingTask implements Runnable { + /** The {@link Call} object from Retrofit. */ + protected ServiceCall serviceCall; + /** The polling state for the current operation. */ + protected PollingState pollingState; + /** The callback used for asynchronous polling. */ + protected ServiceCallback pollingCallback; + /** The client callback to call when polling finishes. */ + protected ServiceCallback clientCallback; + } + + /** + * The task runner that handles PUT or PATCH operations. + * + * @param the return type of the caller. + */ + class PutPatchPollingTask extends AsyncPollingTask { + /** The URL to poll from. */ + private String url; + + /** + * Creates an instance of Polling task for PUT or PATCH operations. + * + * @param pollingState the current polling state. + * @param url the URL to poll from. + * @param serviceCall the ServiceCall object tracking Retrofit calls. + * @param clientCallback the client callback to call when a terminal status is hit. + */ + PutPatchPollingTask(final PollingState pollingState, final String url, final ServiceCall serviceCall, final ServiceCallback clientCallback) { + this.serviceCall = serviceCall; + this.pollingState = pollingState; + this.url = url; + this.clientCallback = clientCallback; + this.pollingCallback = new ServiceCallback() { + @Override + public void failure(Throwable t) { + clientCallback.failure(t); + } + + @Override + public void success(ServiceResponse result) { + PutPatchPollingTask task = new PutPatchPollingTask<>(pollingState, url, serviceCall, clientCallback); + executor.schedule(task, pollingState.getDelayInMilliseconds(), TimeUnit.MILLISECONDS); + } + }; + } + + @Override + public void run() { + // Check provisioning state + if (!AzureAsyncOperation.getTerminalStatuses().contains(pollingState.getStatus())) { + if (pollingState.getAzureAsyncOperationHeaderLink() != null + && !pollingState.getAzureAsyncOperationHeaderLink().isEmpty()) { + this.serviceCall.newCall(updateStateFromAzureAsyncOperationHeaderAsync(pollingState, pollingCallback)); + } else if (pollingState.getLocationHeaderLink() != null + && !pollingState.getLocationHeaderLink().isEmpty()) { + this.serviceCall.newCall(updateStateFromLocationHeaderOnPutAsync(pollingState, pollingCallback)); + } else { + this.serviceCall.newCall(updateStateFromGetResourceOperationAsync(pollingState, url, pollingCallback)); + } + } else { + if (AzureAsyncOperation.SUCCESS_STATUS.equals(pollingState.getStatus()) && pollingState.getResource() == null) { + this.serviceCall.newCall(updateStateFromGetResourceOperationAsync(pollingState, url, clientCallback)); + } else if (AzureAsyncOperation.getFailedStatuses().contains(pollingState.getStatus())) { + clientCallback.failure(new ServiceException("Async operation failed")); + } else { + clientCallback.success(new ServiceResponse<>(pollingState.getResource(), pollingState.getResponse())); + } + } + } + } + + /** + * The task runner that handles POST or DELETE operations. + * + * @param the return type of the caller. + */ + class PostDeletePollingTask extends AsyncPollingTask { + /** + * Creates an instance of Polling task for POST or DELETE operations. + * + * @param pollingState the current polling state. + * @param serviceCall the ServiceCall object tracking Retrofit calls. + * @param clientCallback the client callback to call when a terminal status is hit. + */ + PostDeletePollingTask(final PollingState pollingState, final ServiceCall serviceCall, final ServiceCallback clientCallback) { + this.serviceCall = serviceCall; + this.pollingState = pollingState; + this.clientCallback = clientCallback; + this.pollingCallback = new ServiceCallback() { + @Override + public void failure(Throwable t) { + clientCallback.failure(t); + } + + @Override + public void success(ServiceResponse result) { + PostDeletePollingTask task = new PostDeletePollingTask<>(pollingState, serviceCall, clientCallback); + executor.schedule(task, pollingState.getDelayInMilliseconds(), TimeUnit.MILLISECONDS); + } + }; + } + + @Override + public void run() { + if (!AzureAsyncOperation.getTerminalStatuses().contains(pollingState.getStatus())) { + if (pollingState.getAzureAsyncOperationHeaderLink() != null + && !pollingState.getAzureAsyncOperationHeaderLink().isEmpty()) { + updateStateFromAzureAsyncOperationHeaderAsync(pollingState, pollingCallback); + } else if (pollingState.getLocationHeaderLink() != null + && !pollingState.getLocationHeaderLink().isEmpty()) { + updateStateFromLocationHeaderOnPostOrDeleteAsync(pollingState, pollingCallback); + } else { + pollingCallback.failure(new ServiceException("No header in response")); + } + } else { + // Check if operation failed + if (AzureAsyncOperation.getFailedStatuses().contains(pollingState.getStatus())) { + clientCallback.failure(new ServiceException("Async operation failed")); + } else { + clientCallback.success(new ServiceResponse<>(pollingState.getResource(), pollingState.getResponse())); + } + } + } + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java new file mode 100644 index 0000000000000..04a6af99d93db --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureEnvironment.java @@ -0,0 +1,139 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +/** + * An instance of this class describes an environment in Azure. + */ +public final class AzureEnvironment { + /** + * Base URL for calls to Azure management API. + */ + private final String baseURL; + + /** + * ActiveDirectory Endpoint for the Azure Environment. + */ + private String authenticationEndpoint; + + /** + * Token audience for an endpoint. + */ + private String tokenAudience; + + /** + * Determines whether the authentication endpoint should + * be validated with Azure AD. Default value is true. + */ + private boolean validateAuthority; + + /** + * Initializes an instance of AzureEnvironment class. + * + * @param authenticationEndpoint ActiveDirectory Endpoint for the Azure Environment. + * @param tokenAudience token audience for an endpoint. + * @param validateAuthority whether the authentication endpoint should + * be validated with Azure AD. + * @param baseUrl the base URL for the current environment. + */ + public AzureEnvironment( + String authenticationEndpoint, + String tokenAudience, + boolean validateAuthority, + String baseUrl) { + this.authenticationEndpoint = authenticationEndpoint; + this.tokenAudience = tokenAudience; + this.validateAuthority = validateAuthority; + this.baseURL = baseUrl; + } + + /** + * Provides the settings for authentication with Azure. + */ + public static final AzureEnvironment AZURE = new AzureEnvironment( + "https://login.windows.net/", + "https://management.core.windows.net/", + true, + "https://management.azure.com/"); + + /** + * Provides the settings for authentication with Azure China. + */ + public static final AzureEnvironment AZURE_CHINA = new AzureEnvironment( + "https://login.chinacloudapi.cn/", + "https://management.core.chinacloudapi.cn/", + true, + "https://management.chinacloudapi.cn/"); + + /** + * Provides the settings for authentication with Azure US Government. + */ + public static final AzureEnvironment AZURE_US_GOVERNMENT = new AzureEnvironment( + "https://login.microsoftonline.com/", + "https://management.core.usgovcloudapi.net/", + true, + "https://management.usgovcloudapi.net/"); + + /** + * Provides the settings for authentication with Azure Germany. + */ + public static final AzureEnvironment AZURE_GERMANY = new AzureEnvironment( + "https://login.microsoftonline.de/", + "https://management.core.cloudapi.de/", + true, + "https://management.microsoftazure.de/"); + + /** + * Gets the base URL of the management service. + * + * @return the Base URL for the management service. + */ + public String getBaseUrl() { + return this.baseURL; + } + + /** + * Gets a builder for {@link RestClient}. + * + * @return a builder for the rest client. + */ + public RestClient.Builder.Buildable newRestClientBuilder() { + return new RestClient.Builder() + .withDefaultBaseUrl(this) + .withInterceptor(new RequestIdHeaderInterceptor()); + } + + /** + * Gets the ActiveDirectory Endpoint for the Azure Environment. + * + * @return the ActiveDirectory Endpoint for the Azure Environment. + */ + public String getAuthenticationEndpoint() { + return authenticationEndpoint; + } + + /** + * Gets the token audience for an endpoint. + * + * @return the token audience for an endpoint. + */ + public String getTokenAudience() { + return tokenAudience; + } + + /** + * Gets whether the authentication endpoint should + * be validated with Azure AD. + * + * @return true if the authentication endpoint should be validated with + * Azure AD, false otherwise. + */ + public boolean isValidateAuthority() { + return validateAuthority; + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureResponse.java b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureResponse.java new file mode 100644 index 0000000000000..f1b274c66302f --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureResponse.java @@ -0,0 +1,51 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.ServiceResponse; +import retrofit2.Response; + +/** + * A standard service response including request ID. + * + * @param the type of the response resource + */ +public class AzureResponse extends ServiceResponse { + /** + * Instantiate a ServiceResponse instance with a response object and a raw REST response. + * + * @param body deserialized response object + * @param response raw REST response + */ + public AzureResponse(T body, Response response) { + super(body, response); + } + + /** + * The value that uniquely identifies a request made against the service. + */ + private String requestId; + + /** + * Gets the value that uniquely identifies a request made against the service. + * + * @return the request id value. + */ + public String getRequestId() { + return requestId; + } + + /** + * Sets the value that uniquely identifies a request made against the service. + * + * @param requestId the request id value. + */ + public void setRequestId(String requestId) { + this.requestId = requestId; + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureServiceClient.java b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureServiceClient.java new file mode 100644 index 0000000000000..09c8c28fbd536 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureServiceClient.java @@ -0,0 +1,74 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.serializer.JacksonMapperAdapter; + +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +/** + * ServiceClient is the abstraction for accessing REST operations and their payload data types. + */ +public abstract class AzureServiceClient { + /** + * The RestClient instance storing all information needed for making REST calls. + */ + private RestClient restClient; + + protected AzureServiceClient(String baseUrl) { + this(new RestClient.Builder().withBaseUrl(baseUrl) + .withInterceptor(new RequestIdHeaderInterceptor()).build()); + } + + /** + * Initializes a new instance of the ServiceClient class. + * + * @param restClient the REST client + */ + protected AzureServiceClient(RestClient restClient) { + this.restClient = restClient; + } + + /** + * The default User-Agent header. Override this method to override the user agent. + * + * @return the user agent string. + */ + public String userAgent() { + return "Azure-SDK-For-Java/" + getClass().getPackage().getImplementationVersion(); + } + + /** + * @return the {@link RestClient} instance. + */ + public RestClient restClient() { + return restClient; + } + + /** + * @return the Retrofit instance. + */ + public Retrofit retrofit() { + return restClient().retrofit(); + } + + /** + * @return the HTTP client. + */ + public OkHttpClient httpClient() { + return restClient().httpClient(); + } + + /** + * @return the adapter to a Jackson {@link com.fasterxml.jackson.databind.ObjectMapper}. + */ + public JacksonMapperAdapter mapperAdapter() { + return restClient().mapperAdapter(); + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/AzureServiceResponseBuilder.java b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureServiceResponseBuilder.java new file mode 100644 index 0000000000000..7ad369ecc5686 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/AzureServiceResponseBuilder.java @@ -0,0 +1,73 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.google.common.reflect.TypeToken; +import com.microsoft.rest.RestException; +import com.microsoft.rest.ServiceResponse; +import com.microsoft.rest.ServiceResponseBuilder; +import com.microsoft.rest.serializer.JacksonMapperAdapter; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +import retrofit2.Response; + +/** + * The builder for building a {@link ServiceResponse}. + * + * @param the return type from caller. + * @param the exception to throw in case of error. + */ +public class AzureServiceResponseBuilder extends ServiceResponseBuilder { + /** + * Create a ServiceResponseBuilder instance. + * + * @param deserializer the serialization utils to use for deserialization operations + */ + public AzureServiceResponseBuilder(JacksonMapperAdapter deserializer) { + this(deserializer, new HashMap()); + } + + /** + * Create a ServiceResponseBuilder instance. + * + * @param deserializer the serialization utils to use for deserialization operations + * @param responseTypes a mapping of response status codes and response destination types. + */ + public AzureServiceResponseBuilder(JacksonMapperAdapter deserializer, Map responseTypes) { + super(deserializer, responseTypes); + } + + @SuppressWarnings("unchecked") + @Override + public ServiceResponse buildEmpty(Response response) throws E, IOException { + int statusCode = response.code(); + if (responseTypes.containsKey(statusCode)) { + if (new TypeToken(getClass()) { }.getRawType().isAssignableFrom(Boolean.class)) { + ServiceResponse serviceResponse = new ServiceResponse<>(response); + serviceResponse.setBody((T) (Object) (statusCode / 100 == 2)); + return serviceResponse; + } else { + return new ServiceResponse<>(response); + } + } else { + try { + E exception = (E) exceptionType.getConstructor(String.class).newInstance("Invalid status code " + statusCode); + exceptionType.getMethod("setResponse", response.getClass()).invoke(exception, response); + throw exception; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IOException("Invalid status code " + statusCode + ", but an instance of " + exceptionType.getCanonicalName() + + " cannot be created.", e); + } + } + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/CloudError.java b/azure-client-runtime/src/main/java/com/microsoft/azure/CloudError.java new file mode 100644 index 0000000000000..72f3bcc2e6bb1 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/CloudError.java @@ -0,0 +1,106 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import java.util.ArrayList; +import java.util.List; + +/** + * An instance of this class provides additional information about an http error response. + */ +public class CloudError { + /** + * The error code parsed from the body of the http error response. + */ + private String code; + + /** + * The error message parsed from the body of the http error response. + */ + private String message; + + /** + * The target of the error. + */ + private String target; + + /** + * Details for the error. + */ + private List details; + + /** + * Initializes a new instance of CloudError. + */ + public CloudError() { + this.details = new ArrayList(); + } + + /** + * Gets the error code parsed from the body of the http error response. + * + * @return the error code. + */ + public String getCode() { + return code; + } + + /** + * Sets the error code parsed from the body of the http error response. + * + * @param code the error code. + */ + public void setCode(String code) { + this.code = code; + } + + /** + * Gets the error message parsed from the body of the http error response. + * + * @return the error message. + */ + public String getMessage() { + return message; + } + + /** + * Sets the error message parsed from the body of the http error response. + * + * @param message the error message. + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Gets the target of the error. + * + * @return the target of the error. + */ + public String getTarget() { + return target; + } + + /** + * Sets the target of the error. + * + * @param target the target of the error. + */ + public void setTarget(String target) { + this.target = target; + } + + /** + * Gets the eetails for the error. + * + * @return the details for the error. + */ + public List getDetails() { + return details; + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/CloudException.java b/azure-client-runtime/src/main/java/com/microsoft/azure/CloudException.java new file mode 100644 index 0000000000000..2e0a9db747838 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/CloudException.java @@ -0,0 +1,104 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.RestException; +import retrofit2.Response; + +/** + * Exception thrown for an invalid response with custom error information. + */ +public class CloudException extends RestException { + /** + * Information about the associated HTTP response. + */ + private Response response; + + /** + * The HTTP response body. + */ + private CloudError body; + + /** + * Initializes a new instance of the ServiceException class. + */ + public CloudException() { } + + /** + * Initializes a new instance of the ServiceException class. + * + * @param message The exception message. + */ + public CloudException(final String message) { + super(message); + } + + /** + * Initializes a new instance of the ServiceException class. + * + * @param message the exception message + * @param cause exception that caused this exception to occur + */ + public CloudException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Initializes a new instance of the ServiceException class. + * + * @param cause exception that caused this exception to occur + */ + public CloudException(final Throwable cause) { + super(cause); + } + + /** + * Gets information about the associated HTTP response. + * + * @return the HTTP response + */ + public Response getResponse() { + return response; + } + + /** + * Gets the HTTP response body. + * + * @return the response body + */ + public CloudError getBody() { + return body; + } + + /** + * Sets the HTTP response. + * + * @param response the HTTP response + */ + public void setResponse(Response response) { + this.response = response; + } + + /** + * Sets the HTTP response body. + * + * @param body the response object + */ + public void setBody(CloudError body) { + this.body = body; + } + + @Override + public String toString() { + String message = super.toString(); + if (body != null && body.getMessage() != null) { + message = message + ": " + body.getMessage(); + } + return message; + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/CustomHeaderInterceptor.java b/azure-client-runtime/src/main/java/com/microsoft/azure/CustomHeaderInterceptor.java new file mode 100644 index 0000000000000..7486b838fb633 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/CustomHeaderInterceptor.java @@ -0,0 +1,135 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An instance of this class enables adding custom headers in client requests + * when added to the {@link okhttp3.OkHttpClient} interceptors. + */ +public class CustomHeaderInterceptor implements Interceptor { + /** + * A mapping of custom headers. + */ + private Map> headers; + + /** + * Initialize an instance of {@link CustomHeaderInterceptor} class. + */ + public CustomHeaderInterceptor() { + headers = new HashMap>(); + } + + /** + * Initialize an instance of {@link CustomHeaderInterceptor} class. + * + * @param key the key for the header + * @param value the value of the header + */ + public CustomHeaderInterceptor(String key, String value) { + this(); + addHeader(key, value); + } + + /** + * Add a single header key-value pair. If one with the name already exists, + * it gets replaced. + * + * @param name the name of the header. + * @param value the value of the header. + * @return the interceptor instance itself. + */ + public CustomHeaderInterceptor replaceHeader(String name, String value) { + this.headers.put(name, Collections.singletonList(value)); + return this; + } + + /** + * Add a single header key-value pair. If one with the name already exists, + * both stay in the header map. + * + * @param name the name of the header. + * @param value the value of the header. + * @return the interceptor instance itself. + */ + public CustomHeaderInterceptor addHeader(String name, String value) { + if (this.headers.containsKey(name)) { + this.headers.get(name).add(value); + } else { + this.headers.put(name, Collections.singletonList(value)); + } + return this; + } + + /** + * Add all headers in a {@link Headers} object. + * + * @param headers an OkHttp {@link Headers} object. + * @return the interceptor instance itself. + */ + public CustomHeaderInterceptor addHeaders(Headers headers) { + this.headers.putAll(headers.toMultimap()); + return this; + } + + /** + * Add all headers in a header map. + * + * @param headers a map of headers. + * @return the interceptor instance itself. + */ + public CustomHeaderInterceptor addHeaderMap(Map headers) { + for (Map.Entry header : headers.entrySet()) { + this.headers.put(header.getKey(), Collections.singletonList(header.getValue())); + } + return this; + } + + /** + * Add all headers in a header multimap. + * + * @param headers a multimap of headers. + * @return the interceptor instance itself. + */ + public CustomHeaderInterceptor addHeaderMultimap(Map> headers) { + this.headers.putAll(headers); + return this; + } + + /** + * Remove a header. + * + * @param name the name of the header to remove. + * @return the interceptor instance itself. + */ + public CustomHeaderInterceptor removeHeader(String name) { + this.headers.remove(name); + return this; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request.Builder builder = chain.request().newBuilder(); + for (Map.Entry> header : headers.entrySet()) { + for (String value : header.getValue()) { + builder = builder.header(header.getKey(), value); + } + } + return chain.proceed(builder.build()); + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java new file mode 100644 index 0000000000000..112130413fd87 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGNode.java @@ -0,0 +1,116 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The type representing node in a {@link DAGraph}. + * + * @param the type of the data stored in the node + */ +public class DAGNode extends Node { + private List dependentKeys; + private int toBeResolved; + private boolean isPreparer; + + /** + * Creates a DAG node. + * + * @param key unique id of the node + * @param data data to be stored in the node + */ + public DAGNode(String key, T data) { + super(key, data); + dependentKeys = new ArrayList<>(); + } + + /** + * @return a list of keys of nodes in {@link DAGraph} those are dependents on this node + */ + List dependentKeys() { + return Collections.unmodifiableList(this.dependentKeys); + } + + /** + * Mark the node identified by the given key as dependent of this node. + * + * @param key the id of the dependent node + */ + public void addDependent(String key) { + this.dependentKeys.add(key); + } + + /** + * @return a list of keys of nodes in {@link DAGraph} that this node depends on + */ + public List dependencyKeys() { + return this.children(); + } + + /** + * Mark the node identified by the given key as this node's dependency. + * + * @param dependencyKey the id of the dependency node + */ + public void addDependency(String dependencyKey) { + super.addChild(dependencyKey); + } + + /** + * @return true if this node has any dependency + */ + public boolean hasDependencies() { + return this.hasChildren(); + } + + /** + * Mark or un-mark this node as preparer. + * + * @param isPreparer true if this node needs to be marked as preparer, false otherwise. + */ + public void setPreparer(boolean isPreparer) { + this.isPreparer = isPreparer; + } + + /** + * @return true if this node is marked as preparer + */ + public boolean isPreparer() { + return isPreparer; + } + + /** + * Initialize the node so that traversal can be performed on the parent DAG. + */ + public void initialize() { + this.toBeResolved = this.dependencyKeys().size(); + this.dependentKeys.clear(); + } + + /** + * @return true if all dependencies of this node are ready to be consumed + */ + boolean hasAllResolved() { + return toBeResolved == 0; + } + + /** + * Reports that one of this node's dependency has been resolved and ready to be consumed. + * + * @param dependencyKey the id of the dependency node + */ + void reportResolved(String dependencyKey) { + if (toBeResolved == 0) { + throw new RuntimeException("invalid state - " + this.key() + ": The dependency '" + dependencyKey + "' is already reported or there is no such dependencyKey"); + } + toBeResolved--; + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java new file mode 100644 index 0000000000000..58179e0152ed4 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/DAGraph.java @@ -0,0 +1,179 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import java.util.ArrayDeque; +import java.util.Map; +import java.util.Queue; + +/** + * Type representing a DAG (directed acyclic graph). + *

+ * each node in a DAG is represented by {@link DAGNode} + * + * @param the type of the data stored in the graph nodes + * @param the type of the nodes in the graph + */ +public class DAGraph> extends Graph { + private Queue queue; + private boolean hasParent; + private U rootNode; + + /** + * Creates a new DAG. + * + * @param rootNode the root node of this DAG + */ + public DAGraph(U rootNode) { + this.rootNode = rootNode; + this.queue = new ArrayDeque<>(); + this.rootNode.setPreparer(true); + this.addNode(rootNode); + } + + /** + * @return true if this DAG is merged with another DAG and hence has a parent + */ + public boolean hasParent() { + return hasParent; + } + + /** + * Checks whether the given node is root node of this DAG. + * + * @param node the node {@link DAGNode} to be checked + * @return true if the given node is root node + */ + public boolean isRootNode(U node) { + return this.rootNode == node; + } + + /** + * @return true if this dag is the preparer responsible for + * preparing the DAG for traversal. + */ + public boolean isPreparer() { + return this.rootNode.isPreparer(); + } + + /** + * Merge this DAG with another DAG. + *

+ * This will mark this DAG as a child DAG, the dependencies of nodes in this DAG will be merged + * with (copied to) the parent DAG + * + * @param parent the parent DAG + */ + public void merge(DAGraph parent) { + this.hasParent = true; + parent.rootNode.addDependency(this.rootNode.key()); + for (Map.Entry entry: graph.entrySet()) { + String key = entry.getKey(); + if (!parent.graph.containsKey(key)) { + parent.graph.put(key, entry.getValue()); + } + } + } + + /** + * Prepares this DAG for traversal using getNext method, each call to getNext returns next node + * in the DAG with no dependencies. + */ + public void prepare() { + if (isPreparer()) { + for (U node : graph.values()) { + // Prepare each node for traversal + node.initialize(); + if (!this.isRootNode(node)) { + // Mark other sub-DAGs as non-preparer + node.setPreparer(false); + } + } + initializeDependentKeys(); + initializeQueue(); + } + } + + /** + * Gets next node in the DAG which has no dependency or all of it's dependencies are resolved and + * ready to be consumed. + * + * @return next node or null if all the nodes have been explored + */ + public U getNext() { + return graph.get(queue.poll()); + } + + /** + * Gets the data stored in a graph node with a given key. + * + * @param key the key of the node + * @return the value stored in the node + */ + public T getNodeData(String key) { + return graph.get(key).data(); + } + + /** + * Reports that a node is resolved hence other nodes depends on it can consume it. + * + * @param completed the node ready to be consumed + */ + public void reportedCompleted(U completed) { + completed.setPreparer(true); + String dependency = completed.key(); + for (String dependentKey : graph.get(dependency).dependentKeys()) { + DAGNode dependent = graph.get(dependentKey); + dependent.reportResolved(dependency); + if (dependent.hasAllResolved()) { + queue.add(dependent.key()); + } + } + } + + /** + * Initializes dependents of all nodes. + *

+ * The DAG will be explored in DFS order and all node's dependents will be identified, + * this prepares the DAG for traversal using getNext method, each call to getNext returns next node + * in the DAG with no dependencies. + */ + private void initializeDependentKeys() { + visit(new Visitor() { + // This 'visit' will be called only once per each node. + @Override + public void visit(U node) { + if (node.dependencyKeys().isEmpty()) { + return; + } + + String dependentKey = node.key(); + for (String dependencyKey : node.dependencyKeys()) { + graph.get(dependencyKey) + .addDependent(dependentKey); + } + } + }); + } + + /** + * Initializes the queue that tracks the next set of nodes with no dependencies or + * whose dependencies are resolved. + */ + private void initializeQueue() { + this.queue.clear(); + for (Map.Entry entry: graph.entrySet()) { + if (!entry.getValue().hasDependencies()) { + this.queue.add(entry.getKey()); + } + } + if (queue.isEmpty()) { + throw new RuntimeException("Found circular dependency"); + } + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/Graph.java b/azure-client-runtime/src/main/java/com/microsoft/azure/Graph.java new file mode 100644 index 0000000000000..40ceebaa50b2b --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/Graph.java @@ -0,0 +1,85 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Type representing a directed graph data structure. + *

+ * Each node in a graph is represented by {@link Node} + * + * @param the type of the data stored in the graph's nodes + * @param the type of the nodes in the graph + */ +public class Graph> { + protected Map graph; + private Set visited; + + /** + * Creates a directed graph. + */ + public Graph() { + this.graph = new HashMap<>(); + this.visited = new HashSet<>(); + } + + /** + * Adds a node to this graph. + * + * @param node the node + */ + public void addNode(U node) { + graph.put(node.key(), node); + } + + /** + * Represents a visitor to be implemented by the consumer who want to visit the + * graph's nodes in DFS order. + * + * @param the type of the node + */ + interface Visitor { + /** + * visit a node. + * + * @param node the node to visited + */ + void visit(U node); + } + + /** + * Perform DFS visit in this graph. + *

+ * The directed graph will be traversed in DFS order and the visitor will be notified as + * search explores each node + * + * @param visitor the graph visitor + */ + public void visit(Visitor visitor) { + for (Map.Entry> item : graph.entrySet()) { + if (!visited.contains(item.getKey())) { + this.dfs(visitor, item.getValue()); + } + } + visited.clear(); + } + + private void dfs(Visitor visitor, Node node) { + visitor.visit(node); + visited.add(node.key()); + for (String childKey : node.children()) { + if (!visited.contains(childKey)) { + this.dfs(visitor, this.graph.get(childKey)); + } + } + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/ListOperationCallback.java b/azure-client-runtime/src/main/java/com/microsoft/azure/ListOperationCallback.java new file mode 100644 index 0000000000000..6e0b2ea92a96b --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/ListOperationCallback.java @@ -0,0 +1,96 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.ServiceCallback; + +import java.util.List; + +/** + * The callback used for client side asynchronous list operations. + * + * @param the item type + */ +public abstract class ListOperationCallback extends ServiceCallback> { + /** + * A list result that stores the accumulated resources loaded from server. + */ + private List result; + + /** + * Number of loaded pages. + */ + private int pageCount; + + /** + * Creates an instance of ListOperationCallback. + */ + public ListOperationCallback() { + this.pageCount = 0; + } + + /** + * Override this method to handle progressive results. + * The user is responsible for returning a {@link PagingBahavior} Enum to indicate + * whether the client should continue loading or stop. + * + * @param partial the list of resources from the current request. + * @return CONTINUE if you want to go on loading, STOP otherwise. + * + */ + public PagingBahavior progress(List partial) { + return PagingBahavior.CONTINUE; + } + + /** + * Get the list result that stores the accumulated resources loaded from server. + * + * @return the list of resources. + */ + public List get() { + return result; + } + + /** + * This method is called by the client to load the most recent list of resources. + * This method should only be called by the service client. + * + * @param result the most recent list of resources. + */ + public void load(List result) { + ++pageCount; + if (this.result == null || this.result.isEmpty()) { + this.result = result; + } else { + this.result.addAll(result); + } + } + + /** + * Get the number of loaded pages. + * + * @return the number of pages. + */ + public int pageCount() { + return pageCount; + } + + /** + * An enum to indicate whether the client should continue loading or stop. + */ + public enum PagingBahavior { + /** + * Indicates that the client should continue loading. + */ + CONTINUE, + /** + * Indicates that the client should stop loading. + */ + STOP + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/Node.java b/azure-client-runtime/src/main/java/com/microsoft/azure/Node.java new file mode 100644 index 0000000000000..cbb83f1d2a58f --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/Node.java @@ -0,0 +1,70 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Type represents a node in a {@link Graph}. + * + * @param the type of the data stored in the node + */ +public class Node { + private String key; + private T data; + private List children; + + /** + * Creates a graph node. + * + * @param key unique id of the node + * @param data data to be stored in the node + */ + public Node(String key, T data) { + this.key = key; + this.data = data; + this.children = new ArrayList<>(); + } + + /** + * @return this node's unique id + */ + public String key() { + return this.key; + } + + /** + * @return data stored in this node + */ + public T data() { + return data; + } + + /** + * @return true if this node has any children + */ + public boolean hasChildren() { + return !this.children.isEmpty(); + } + + /** + * @return children (neighbours) of this node + */ + public List children() { + return Collections.unmodifiableList(this.children); + } + + /** + * @param childKey add a child (neighbour) of this node + */ + public void addChild(String childKey) { + this.children.add(childKey); + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/Page.java b/azure-client-runtime/src/main/java/com/microsoft/azure/Page.java new file mode 100644 index 0000000000000..0d3c04eb9215a --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/Page.java @@ -0,0 +1,31 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import java.util.List; + +/** + * Defines a page interface in Azure responses. + * + * @param the element type. + */ +public interface Page { + /** + * Gets the link to the next page. + * + * @return the link. + */ + String getNextPageLink(); + + /** + * Gets the list of items. + * + * @return the list of items. + */ + List getItems(); +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/PagedList.java b/azure-client-runtime/src/main/java/com/microsoft/azure/PagedList.java new file mode 100644 index 0000000000000..a88a044f751e7 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/PagedList.java @@ -0,0 +1,339 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.RestException; + +import javax.xml.bind.DataBindingException; +import javax.xml.ws.WebServiceException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; + +/** + * Defines a list response from a paging operation. The pages are + * lazy initialized when an instance of this class is iterated. + * + * @param the element type. + */ +public abstract class PagedList implements List { + /** The actual items in the list. */ + private List items; + /** Stores the link to get the next page of items. */ + private String nextPageLink; + /** Stores the latest page fetched. */ + private Page currentPage; + + /** + * Creates an instance of Pagedlist. + */ + public PagedList() { + items = new ArrayList<>(); + } + + /** + * Creates an instance of PagedList from a {@link Page} response. + * + * @param page the {@link Page} object. + */ + public PagedList(Page page) { + this(); + items.addAll(page.getItems()); + nextPageLink = page.getNextPageLink(); + currentPage = page; + } + + /** + * Override this method to load the next page of items from a next page link. + * + * @param nextPageLink the link to get the next page of items. + * @return the {@link Page} object storing a page of items and a link to the next page. + * @throws RestException thrown if an error is raised from Azure. + * @throws IOException thrown if there's any failure in deserialization. + */ + public abstract Page nextPage(String nextPageLink) throws RestException, IOException; + + /** + * If there are more pages available. + * + * @return true if there are more pages to load. False otherwise. + */ + public boolean hasNextPage() { + return this.nextPageLink != null; + } + + /** + * Loads a page from next page link. + * The exceptions are wrapped into Java Runtime exceptions. + */ + public void loadNextPage() { + try { + Page nextPage = nextPage(this.nextPageLink); + this.nextPageLink = nextPage.getNextPageLink(); + this.items.addAll(nextPage.getItems()); + this.currentPage = nextPage; + } catch (RestException e) { + throw new WebServiceException(e.toString(), e); + } catch (IOException e) { + throw new DataBindingException(e.getMessage(), e); + } + } + + /** + * Keep loading the next page from the next page link until all items are loaded. + */ + public void loadAll() { + while (hasNextPage()) { + loadNextPage(); + } + } + + /** + * Gets the latest page fetched. + * + * @return the latest page. + */ + public Page currentPage() { + return currentPage; + } + + /** + * Gets the next page's link. + * + * @return the next page link. + */ + public String nextPageLink() { + return nextPageLink; + } + + /** + * The implementation of {@link ListIterator} for PagedList. + */ + private class ListItr implements ListIterator { + /** The list iterator for the actual list of items. */ + private ListIterator itemsListItr; + + /** + * Creates an instance of the ListIterator. + * + * @param index the position in the list to start. + */ + ListItr(int index) { + itemsListItr = items.listIterator(index); + } + + @Override + public boolean hasNext() { + return itemsListItr.hasNext() || hasNextPage(); + } + + @Override + public E next() { + if (!itemsListItr.hasNext()) { + if (!hasNextPage()) { + throw new NoSuchElementException(); + } else { + int size = items.size(); + loadNextPage(); + itemsListItr = items.listIterator(size); + } + } + return itemsListItr.next(); + } + + @Override + public void remove() { + itemsListItr.remove(); + } + + @Override + public boolean hasPrevious() { + return itemsListItr.hasPrevious(); + } + + @Override + public E previous() { + return itemsListItr.previous(); + } + + @Override + public int nextIndex() { + return itemsListItr.nextIndex(); + } + + @Override + public int previousIndex() { + return itemsListItr.previousIndex(); + } + + @Override + public void set(E e) { + itemsListItr.set(e); + } + + @Override + public void add(E e) { + itemsListItr.add(e); + } + } + + @Override + public int size() { + loadAll(); + return items.size(); + } + + @Override + public boolean isEmpty() { + return items.isEmpty() && !hasNextPage(); + } + + @Override + public boolean contains(Object o) { + return indexOf(o) >= 0; + } + + @Override + public Iterator iterator() { + return new ListItr(0); + } + + @Override + public Object[] toArray() { + loadAll(); + return items.toArray(); + } + + @Override + public T[] toArray(T[] a) { + loadAll(); + return items.toArray(a); + } + + @Override + public boolean add(E e) { + return items.add(e); + } + + @Override + public boolean remove(Object o) { + return items.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + for (Object e : c) { + if (!contains(e)) { + return false; + } + } + return true; + } + + @Override + public boolean addAll(Collection c) { + return items.addAll(c); + } + + @Override + public boolean addAll(int index, Collection c) { + return items.addAll(index, c); + } + + @Override + public boolean removeAll(Collection c) { + return items.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return items.retainAll(c); + } + + @Override + public void clear() { + items.clear(); + } + + @Override + public E get(int index) { + while (index >= items.size() && hasNextPage()) { + loadNextPage(); + } + return items.get(index); + } + + @Override + public E set(int index, E element) { + return items.set(index, element); + } + + @Override + public void add(int index, E element) { + items.add(index, element); + } + + @Override + public E remove(int index) { + return items.remove(index); + } + + @Override + public int indexOf(Object o) { + int index = 0; + if (o == null) { + for (E item : this) { + if (item == null) { + return index; + } + ++index; + } + } else { + for (E item : this) { + if (item == o) { + return index; + } + ++index; + } + } + return -1; + } + + @Override + public int lastIndexOf(Object o) { + loadAll(); + return items.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return new ListItr(0); + } + + @Override + public ListIterator listIterator(int index) { + while (index >= items.size() && hasNextPage()) { + loadNextPage(); + } + return new ListItr(index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + while ((fromIndex >= items.size() + || toIndex >= items.size()) + && hasNextPage()) { + loadNextPage(); + } + return items.subList(fromIndex, toIndex); + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/PollingState.java b/azure-client-runtime/src/main/java/com/microsoft/azure/PollingState.java new file mode 100644 index 0000000000000..50255ba600899 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/PollingState.java @@ -0,0 +1,316 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.rest.serializer.JacksonMapperAdapter; + +import java.io.IOException; +import java.lang.reflect.Type; + +import okhttp3.ResponseBody; +import retrofit2.Response; + +/** + * An instance of this class defines the state of a long running operation. + * + * @param the type of the resource the operation returns. + */ +public class PollingState { + /** The Retrofit response object. */ + private Response response; + /** The polling status. */ + private String status; + /** The link in 'Azure-AsyncOperation' header. */ + private String azureAsyncOperationHeaderLink; + /** The link in 'Location' Header. */ + private String locationHeaderLink; + /** The timeout interval between two polling operations. */ + private Integer retryTimeout; + /** The response resource object. */ + private T resource; + /** The type of the response resource object. */ + private Type resourceType; + /** The error during the polling operations. */ + private CloudError error; + /** The adapter for {@link com.fasterxml.jackson.databind.ObjectMapper}. */ + private JacksonMapperAdapter mapperAdapter; + + /** + * Initializes an instance of {@link PollingState}. + * + * @param response the response from Retrofit REST call. + * @param retryTimeout the long running operation retry timeout. + * @param resourceType the type of the resource the long running operation returns + * @param mapperAdapter the adapter for the Jackson object mapper + * @throws IOException thrown by deserialization + */ + public PollingState(Response response, Integer retryTimeout, Type resourceType, JacksonMapperAdapter mapperAdapter) throws IOException { + this.retryTimeout = retryTimeout; + this.setResponse(response); + this.resourceType = resourceType; + this.mapperAdapter = mapperAdapter; + + String responseContent = null; + PollingResource resource = null; + if (response.body() != null) { + responseContent = response.body().string(); + response.body().close(); + } + if (responseContent != null && !responseContent.isEmpty()) { + this.resource = mapperAdapter.deserialize(responseContent, resourceType); + resource = mapperAdapter.deserialize(responseContent, PollingResource.class); + } + if (resource != null && resource.getProperties() != null + && resource.getProperties().getProvisioningState() != null) { + setStatus(resource.getProperties().getProvisioningState()); + } else { + switch (this.response.code()) { + case 202: + setStatus(AzureAsyncOperation.IN_PROGRESS_STATUS); + break; + case 204: + case 201: + case 200: + setStatus(AzureAsyncOperation.SUCCESS_STATUS); + break; + default: + setStatus(AzureAsyncOperation.FAILED_STATUS); + } + } + } + + /** + * Updates the polling state from a PUT or PATCH operation. + * + * @param response the response from Retrofit REST call + * @throws CloudException thrown if the response is invalid + * @throws IOException thrown by deserialization + */ + public void updateFromResponseOnPutPatch(Response response) throws CloudException, IOException { + String responseContent = null; + if (response.body() != null) { + responseContent = response.body().string(); + response.body().close(); + } + + if (responseContent == null || responseContent.isEmpty()) { + CloudException exception = new CloudException("no body"); + exception.setResponse(response); + throw exception; + } + + PollingResource resource = mapperAdapter.deserialize(responseContent, PollingResource.class); + if (resource != null && resource.getProperties() != null && resource.getProperties().getProvisioningState() != null) { + this.setStatus(resource.getProperties().getProvisioningState()); + } else { + this.setStatus(AzureAsyncOperation.SUCCESS_STATUS); + } + + CloudError error = new CloudError(); + this.setError(error); + error.setCode(this.getStatus()); + error.setMessage("Long running operation failed"); + this.setResponse(response); + this.setResource(mapperAdapter.deserialize(responseContent, resourceType)); + } + + /** + * Updates the polling state from a DELETE or POST operation. + * + * @param response the response from Retrofit REST call + * @throws IOException thrown by deserialization + */ + + public void updateFromResponseOnDeletePost(Response response) throws IOException { + this.setResponse(response); + String responseContent = null; + if (response.body() != null) { + responseContent = response.body().string(); + response.body().close(); + } + this.setResource(mapperAdapter.deserialize(responseContent, resourceType)); + setStatus(AzureAsyncOperation.SUCCESS_STATUS); + } + + /** + * Gets long running operation delay in milliseconds. + * + * @return the delay in milliseconds. + */ + public int getDelayInMilliseconds() { + if (this.retryTimeout != null) { + return this.retryTimeout * 1000; + } + if (this.response != null && response.headers().get("Retry-After") != null) { + return Integer.parseInt(response.headers().get("Retry-After")) * 1000; + } + return AzureAsyncOperation.DEFAULT_DELAY * 1000; + } + + /** + * Gets the polling status. + * + * @return the polling status. + */ + public String getStatus() { + return status; + } + + + /** + * Sets the polling status. + * + * @param status the polling status. + * @throws IllegalArgumentException thrown if status is null. + */ + public void setStatus(String status) throws IllegalArgumentException { + if (status == null) { + throw new IllegalArgumentException("Status is null."); + } + this.status = status; + } + + /** + * Gets the last operation response. + * + * @return the last operation response. + */ + public Response getResponse() { + return this.response; + } + + + /** + * Sets the last operation response. + * + * @param response the last operation response. + */ + public void setResponse(Response response) { + this.response = response; + if (response != null) { + String asyncHeader = response.headers().get("Azure-AsyncOperation"); + String locationHeader = response.headers().get("Location"); + if (asyncHeader != null) { + this.azureAsyncOperationHeaderLink = asyncHeader; + } + if (locationHeader != null) { + this.locationHeaderLink = locationHeader; + } + } + } + + /** + * Gets the latest value captured from Azure-AsyncOperation header. + * + * @return the link in the header. + */ + public String getAzureAsyncOperationHeaderLink() { + return azureAsyncOperationHeaderLink; + } + + /** + * Gets the latest value captured from Location header. + * + * @return the link in the header. + */ + public String getLocationHeaderLink() { + return locationHeaderLink; + } + + /** + * Gets the resource. + * + * @return the resource. + */ + public T getResource() { + return resource; + } + + /** + * Sets the resource. + * + * @param resource the resource. + */ + public void setResource(T resource) { + this.resource = resource; + } + + /** + * Gets {@link CloudError} from current instance. + * + * @return the cloud error. + */ + public CloudError getError() { + return error; + } + + /** + * Sets {@link CloudError} from current instance. + * + * @param error the cloud error. + */ + public void setError(CloudError error) { + this.error = error; + } + + /** + * An instance of this class describes the status of a long running operation + * and is returned from server each time. + */ + static class PollingResource { + /** Inner properties object. */ + @JsonProperty(value = "properties") + private Properties properties; + + /** + * Gets the inner properties object. + * + * @return the inner properties. + */ + public Properties getProperties() { + return properties; + } + + /** + * Sets the inner properties object. + * + * @param properties the inner properties. + */ + public void setProperties(Properties properties) { + this.properties = properties; + } + + /** + * Inner properties class. + */ + static class Properties { + /** The provisioning state of the resource. */ + @JsonProperty(value = "provisioningState") + private String provisioningState; + + /** + * Gets the provisioning state of the resource. + * + * @return the provisioning state. + */ + public String getProvisioningState() { + return provisioningState; + } + + /** + * Sets the provisioning state of the resource. + * + * @param provisioningState the provisioning state. + */ + public void setProvisioningState(String provisioningState) { + this.provisioningState = provisioningState; + } + } + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/RequestIdHeaderInterceptor.java b/azure-client-runtime/src/main/java/com/microsoft/azure/RequestIdHeaderInterceptor.java new file mode 100644 index 0000000000000..ddbe0db30ec0b --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/RequestIdHeaderInterceptor.java @@ -0,0 +1,29 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; +import java.util.UUID; + +/** + * An instance of this class puts an UUID in the request header. Azure uses + * the request id as the unique identifier for + */ +public class RequestIdHeaderInterceptor implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request().newBuilder() + .header("x-ms-client-request-id", UUID.randomUUID().toString()) + .build(); + return chain.proceed(request); + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/Resource.java b/azure-client-runtime/src/main/java/com/microsoft/azure/Resource.java new file mode 100644 index 0000000000000..2f98e586a960d --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/Resource.java @@ -0,0 +1,113 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +/** + * The Resource model. + */ +public class Resource { + /** + * Resource Id. + */ + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String id; + + /** + * Resource name. + */ + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String name; + + /** + * Resource type. + */ + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String type; + + /** + * Resource location. + */ + @JsonProperty(required = true) + private String location; + + /** + * Resource tags. + */ + private Map tags; + + /** + * Get the id value. + * + * @return the id value + */ + public String id() { + return this.id; + } + + /** + * Get the name value. + * + * @return the name value + */ + public String name() { + return this.name; + } + + /** + * Get the type value. + * + * @return the type value + */ + public String type() { + return this.type; + } + + /** + * Get the location value. + * + * @return the location value + */ + public String location() { + return this.location; + } + + /** + * Set the location value. + * + * @param location the location value to set + * @return the resource itself + */ + public Resource withLocation(String location) { + this.location = location; + return this; + } + + /** + * Get the tags value. + * + * @return the tags value + */ + public Map getTags() { + return this.tags; + } + + /** + * Set the tags value. + * + * @param tags the tags value to set + * @return the resource itself + */ + public Resource withTags(Map tags) { + this.tags = tags; + return this; + } +} \ No newline at end of file diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/ResourceGetExponentialBackoffRetryStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/ResourceGetExponentialBackoffRetryStrategy.java new file mode 100644 index 0000000000000..a145fecc6795b --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/ResourceGetExponentialBackoffRetryStrategy.java @@ -0,0 +1,48 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.retry.RetryStrategy; +import okhttp3.Response; + +/** + * A retry strategy with backoff parameters for calculating the exponential + * delay between retries for 404s from GET calls. + */ +public class ResourceGetExponentialBackoffRetryStrategy extends RetryStrategy { + /** + * Represents the default number of retries. + */ + private static final int DEFAULT_NUMBER_OF_ATTEMPTS = 3; + + /** + * Creates an instance of the retry strategy. + */ + public ResourceGetExponentialBackoffRetryStrategy() { + this(null, DEFAULT_FIRST_FAST_RETRY); + } + + /** + * Initializes a new instance of the {@link RetryStrategy} class. + * + * @param name The name of the retry strategy. + * @param firstFastRetry true to immediately retry in the first attempt; otherwise, false. + */ + private ResourceGetExponentialBackoffRetryStrategy(String name, boolean firstFastRetry) { + super(name, firstFastRetry); + } + + @Override + public boolean shouldRetry(int retryCount, Response response) { + int code = response.code(); + //CHECKSTYLE IGNORE MagicNumber FOR NEXT 2 LINES + return retryCount < DEFAULT_NUMBER_OF_ATTEMPTS + && code == 404 + && response.request().method().equalsIgnoreCase("GET"); + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java b/azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java new file mode 100644 index 0000000000000..2cd137953fe7b --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/RestClient.java @@ -0,0 +1,352 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.azure.serializer.AzureJacksonMapperAdapter; +import com.microsoft.rest.BaseUrlHandler; +import com.microsoft.rest.CustomHeadersInterceptor; +import com.microsoft.rest.UserAgentInterceptor; +import com.microsoft.rest.credentials.ServiceClientCredentials; +import com.microsoft.rest.retry.RetryHandler; +import com.microsoft.rest.serializer.JacksonMapperAdapter; + +import java.lang.reflect.Field; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.Proxy; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import okhttp3.ConnectionPool; +import okhttp3.Interceptor; +import okhttp3.JavaNetCookieJar; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; + +/** + * An instance of this class stores the client information for making REST calls. + */ +public class RestClient { + /** The {@link okhttp3.OkHttpClient} object. */ + private OkHttpClient httpClient; + /** The {@link retrofit2.Retrofit} object. */ + private Retrofit retrofit; + /** The credentials to authenticate. */ + private ServiceClientCredentials credentials; + /** The interceptor to handle custom headers. */ + private CustomHeadersInterceptor customHeadersInterceptor; + /** The interceptor to handle base URL. */ + private BaseUrlHandler baseUrlHandler; + /** The adapter to a Jackson {@link com.fasterxml.jackson.databind.ObjectMapper}. */ + private JacksonMapperAdapter mapperAdapter; + /** The interceptor to set 'User-Agent' header. */ + private UserAgentInterceptor userAgentInterceptor; + + protected RestClient(OkHttpClient httpClient, + Retrofit retrofit, + ServiceClientCredentials credentials, + CustomHeadersInterceptor customHeadersInterceptor, + UserAgentInterceptor userAgentInterceptor, + BaseUrlHandler baseUrlHandler, + JacksonMapperAdapter mapperAdapter) { + this.httpClient = httpClient; + this.retrofit = retrofit; + this.credentials = credentials; + this.customHeadersInterceptor = customHeadersInterceptor; + this.userAgentInterceptor = userAgentInterceptor; + this.baseUrlHandler = baseUrlHandler; + this.mapperAdapter = mapperAdapter; + } + + /** + * Get the headers interceptor. + * + * @return the headers interceptor. + */ + public CustomHeadersInterceptor headers() { + return customHeadersInterceptor; + } + + /** + * Get the adapter to {@link com.fasterxml.jackson.databind.ObjectMapper}. + * + * @return the Jackson mapper adapter. + */ + public JacksonMapperAdapter mapperAdapter() { + return mapperAdapter; + } + + /** + * Sets the mapper adapter. + * + * @param mapperAdapter an adapter to a Jackson mapper. + * @return the builder itself for chaining. + */ + public RestClient withMapperAdapater(JacksonMapperAdapter mapperAdapter) { + this.mapperAdapter = mapperAdapter; + return this; + } + + /** + * Get the http client. + * + * @return the {@link OkHttpClient} object. + */ + public OkHttpClient httpClient() { + return httpClient; + } + + /** + * Get the retrofit instance. + * + * @return the {@link Retrofit} object. + */ + public Retrofit retrofit() { + return retrofit; + } + + /** + * Get the credentials attached to this REST client. + * + * @return the credentials. + */ + public ServiceClientCredentials credentials() { + return this.credentials; + } + + /** + * The builder class for building a REST client. + */ + public static class Builder { + /** The dynamic base URL with variables wrapped in "{" and "}". */ + protected String baseUrl; + /** The builder to build an {@link OkHttpClient}. */ + protected OkHttpClient.Builder httpClientBuilder; + /** The builder to build a {@link Retrofit}. */ + protected Retrofit.Builder retrofitBuilder; + /** The credentials to authenticate. */ + protected ServiceClientCredentials credentials; + /** The interceptor to handle custom headers. */ + protected CustomHeadersInterceptor customHeadersInterceptor; + /** The interceptor to handle base URL. */ + protected BaseUrlHandler baseUrlHandler; + /** The interceptor to set 'User-Agent' header. */ + protected UserAgentInterceptor userAgentInterceptor; + /** The inner Builder instance. */ + protected Buildable buildable; + + /** + * Creates an instance of the builder with a base URL to the service. + */ + public Builder() { + this(new OkHttpClient.Builder(), new Retrofit.Builder()); + } + + /** + * Creates an instance of the builder with a base URL and 2 custom builders. + * + * @param httpClientBuilder the builder to build an {@link OkHttpClient}. + * @param retrofitBuilder the builder to build a {@link Retrofit}. + */ + public Builder(OkHttpClient.Builder httpClientBuilder, Retrofit.Builder retrofitBuilder) { + if (httpClientBuilder == null) { + throw new IllegalArgumentException("httpClientBuilder == null"); + } + if (retrofitBuilder == null) { + throw new IllegalArgumentException("retrofitBuilder == null"); + } + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); + customHeadersInterceptor = new CustomHeadersInterceptor(); + baseUrlHandler = new BaseUrlHandler(); + userAgentInterceptor = new UserAgentInterceptor(); + // Set up OkHttp client + this.httpClientBuilder = httpClientBuilder + .cookieJar(new JavaNetCookieJar(cookieManager)) + .addInterceptor(userAgentInterceptor); + this.retrofitBuilder = retrofitBuilder; + this.buildable = new Buildable(); + } + + /** + * Sets the dynamic base URL. + * + * @param baseUrl the base URL to use. + * @return the builder itself for chaining. + */ + public Buildable withBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return buildable; + } + + /** + * Sets the base URL with the default from the service client. + * + * @param serviceClientClass the service client class containing a default base URL. + * @return the builder itself for chaining. + */ + public Buildable withDefaultBaseUrl(Class serviceClientClass) { + try { + Field field = serviceClientClass.getDeclaredField("DEFAULT_BASE_URL"); + field.setAccessible(true); + baseUrl = (String) field.get(null); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new UnsupportedOperationException("Cannot read static field DEFAULT_BASE_URL", e); + } + return buildable; + } + + /** + * Sets the base URL with the default from the Azure Environment. + * + * @param environment the environment the application is running in + * @return the builder itself for chaining + */ + public RestClient.Builder.Buildable withDefaultBaseUrl(AzureEnvironment environment) { + withBaseUrl(environment.getBaseUrl()); + return buildable; + } + + /** + * The inner class from which a Rest Client can be built. + */ + public class Buildable { + /** + * Sets the user agent header. + * + * @param userAgent the user agent header. + * @return the builder itself for chaining. + */ + public Buildable withUserAgent(String userAgent) { + userAgentInterceptor.withUserAgent(userAgent); + return this; + } + + /** + * Sets the credentials. + * + * @param credentials the credentials object. + * @return the builder itself for chaining. + */ + public Buildable withCredentials(ServiceClientCredentials credentials) { + Builder.this.credentials = credentials; + if (credentials != null) { + credentials.applyCredentialsFilter(httpClientBuilder); + } + return this; + } + + /** + * Sets the log level. + * + * @param logLevel the {@link okhttp3.logging.HttpLoggingInterceptor.Level} enum. + * @return the builder itself for chaining. + */ + public Buildable withLogLevel(HttpLoggingInterceptor.Level logLevel) { + httpClientBuilder.addInterceptor(new HttpLoggingInterceptor().setLevel(logLevel)); + return this; + } + + /** + * Add an interceptor the Http client pipeline. + * + * @param interceptor the interceptor to add. + * @return the builder itself for chaining. + */ + public Buildable withInterceptor(Interceptor interceptor) { + httpClientBuilder.addInterceptor(interceptor); + return this; + } + + /** + * Set the read timeout on the HTTP client. Default is 10 seconds. + * + * @param timeout the timeout numeric value + * @param unit the time unit for the numeric value + * @return the builder itself for chaining + */ + public Buildable withReadTimeout(long timeout, TimeUnit unit) { + httpClientBuilder.readTimeout(timeout, unit); + return this; + } + + /** + * Set the connection timeout on the HTTP client. Default is 10 seconds. + * + * @param timeout the timeout numeric value + * @param unit the time unit for the numeric value + * @return the builder itself for chaining + */ + public Buildable withConnectionTimeout(long timeout, TimeUnit unit) { + httpClientBuilder.connectTimeout(timeout, unit); + return this; + } + + /** + * Set the maximum idle connections for the HTTP client. Default is 5. + * + * @param maxIdleConnections the maximum idle connections + * @return the builder itself for chaining + */ + public Buildable withMaxIdleConnections(int maxIdleConnections) { + httpClientBuilder.connectionPool(new ConnectionPool(maxIdleConnections, 5, TimeUnit.MINUTES)); + return this; + } + + /** + * Sets the executor for async callbacks to run on. + * + * @param executor the executor to execute the callbacks. + * @return the builder itself for chaining + */ + public Buildable withCallbackExecutor(Executor executor) { + retrofitBuilder.callbackExecutor(executor); + return this; + } + + /** + * Sets the proxy for the HTTP client. + * + * @param proxy the proxy to use + * @return the builder itself for chaining + */ + public Buildable withProxy(Proxy proxy) { + httpClientBuilder.proxy(proxy); + return this; + } + + /** + * Build a RestClient with all the current configurations. + * + * @return a {@link RestClient}. + */ + public RestClient build() { + AzureJacksonMapperAdapter mapperAdapter = new AzureJacksonMapperAdapter(); + OkHttpClient httpClient = httpClientBuilder + .addInterceptor(baseUrlHandler) + .addInterceptor(customHeadersInterceptor) + .addInterceptor(new RetryHandler(new ResourceGetExponentialBackoffRetryStrategy())) + .addInterceptor(new RetryHandler()) + .build(); + return new RestClient(httpClient, + retrofitBuilder + .baseUrl(baseUrl) + .client(httpClient) + .addConverterFactory(mapperAdapter.getConverterFactory()) + .build(), + credentials, + customHeadersInterceptor, + userAgentInterceptor, + baseUrlHandler, + mapperAdapter); + } + + } + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/SubResource.java b/azure-client-runtime/src/main/java/com/microsoft/azure/SubResource.java new file mode 100644 index 0000000000000..12bedc30c57fc --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/SubResource.java @@ -0,0 +1,38 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +/** + * The SubResource model. + */ +public class SubResource { + /** + * Resource Id. + */ + private String id; + + /** + * Get the id value. + * + * @return the id value + */ + public String id() { + return this.id; + } + + /** + * Set the id value. + * + * @param id the id value to set + * @return the sub resource itself + */ + public SubResource withId(String id) { + this.id = id; + return this; + } +} \ No newline at end of file diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java new file mode 100644 index 0000000000000..26bbbece2606b --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroup.java @@ -0,0 +1,77 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.ServiceCall; +import com.microsoft.rest.ServiceCallback; + +/** + * Represents a group of related tasks. + *

+ * each task in a group is represented by {@link TaskItem} + * + * @param the type of result of tasks in the group + * @param the task type + */ +public interface TaskGroup> { + /** + * Gets underlying directed acyclic graph structure that stores tasks in the group and + * dependency information between them. + * + * @return the dag + */ + DAGraph> dag(); + + /** + * Merges this task group with parent task group. + *

+ * once merged, calling execute in the parent group will executes the task in this + * group as well. + * + * @param parentTaskGroup task group + */ + void merge(TaskGroup parentTaskGroup); + + /** + * @return true if the group is responsible for preparing execution of original task in + * this group and all tasks belong other task group it composes. + */ + boolean isPreparer(); + + /** + * Prepare the graph for execution. + */ + void prepare(); + + /** + * Executes the tasks in the group. + *

+ * the order of execution of tasks ensure that a task gets selected for execution only after + * the execution of all the tasks it depends on + * @throws Exception the exception + */ + void execute() throws Exception; + + /** + * Executes the tasks in the group asynchronously. + * + * @param callback the callback to call on failure or success + * @return the handle to the REST call + */ + ServiceCall executeAsync(ServiceCallback callback); + + /** + * Gets the result of execution of a task in the group. + *

+ * this method can null if the task has not yet been executed + * + * @param taskId the task id + * @return the task result + */ + T taskResult(String taskId); +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java new file mode 100644 index 0000000000000..efc8d30e491bc --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskGroupBase.java @@ -0,0 +1,111 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.ServiceCall; +import com.microsoft.rest.ServiceCallback; + +/** + * The base implementation of TaskGroup interface. + * + * @param the result type of the tasks in the group + */ +public abstract class TaskGroupBase + implements TaskGroup> { + private DAGraph, DAGNode>> dag; + + /** + * Creates TaskGroupBase. + * + * @param rootTaskItemId the id of the root task in this task group + * @param rootTaskItem the root task + */ + public TaskGroupBase(String rootTaskItemId, TaskItem rootTaskItem) { + this.dag = new DAGraph<>(new DAGNode<>(rootTaskItemId, rootTaskItem)); + } + + @Override + public DAGraph, DAGNode>> dag() { + return dag; + } + + @Override + public boolean isPreparer() { + return dag.isPreparer(); + } + + @Override + public void merge(TaskGroup> parentTaskGroup) { + dag.merge(parentTaskGroup.dag()); + } + + @Override + public void prepare() { + if (isPreparer()) { + dag.prepare(); + } + } + + @Override + public void execute() throws Exception { + DAGNode> nextNode = dag.getNext(); + if (nextNode == null) { + return; + } + + if (dag.isRootNode(nextNode)) { + executeRootTask(nextNode.data()); + } else { + nextNode.data().execute(this, nextNode); + } + } + + @Override + public ServiceCall executeAsync(final ServiceCallback callback) { + final DAGNode> nextNode = dag.getNext(); + if (nextNode == null) { + return null; + } + + if (dag.isRootNode(nextNode)) { + return executeRootTaskAsync(nextNode.data(), callback); + } else { + return nextNode.data().executeAsync(this, nextNode, callback); + } + } + + @Override + public T taskResult(String taskId) { + return dag.getNodeData(taskId).result(); + } + + /** + * Executes the root task in this group. + *

+ * This method will be invoked when all the task dependencies of the root task are finished + * executing, at this point root task can be executed by consuming the result of tasks it + * depends on. + * + * @param task the root task in this group + * @throws Exception the exception + */ + public abstract void executeRootTask(TaskItem task) throws Exception; + + /** + * Executes the root task in this group asynchronously. + *

+ * This method will be invoked when all the task dependencies of the root task are finished + * executing, at this point root task can be executed by consuming the result of tasks it + * depends on. + * + * @param task the root task in this group + * @param callback the callback when the task fails or succeeds + * @return the handle to the REST call + */ + public abstract ServiceCall executeRootTaskAsync(TaskItem task, ServiceCallback callback); +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java new file mode 100644 index 0000000000000..fb74c23845c3b --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/TaskItem.java @@ -0,0 +1,46 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.ServiceCall; +import com.microsoft.rest.ServiceCallback; + +/** + * Type representing a task in a task group {@link TaskGroup}. + * + * @param the task result type + */ +public interface TaskItem { + /** + * @return the result of the task execution + */ + U result(); + + /** + * Executes the task. + *

+ * once executed the result will be available through result getter + * + * @param taskGroup the task group dispatching tasks + * @param node the node the task item is associated with + * @throws Exception exception + */ + void execute(TaskGroup> taskGroup, DAGNode> node) throws Exception; + + /** + * Executes the task asynchronously. + *

+ * once executed the result will be available through result getter + + * @param taskGroup the task group dispatching tasks + * @param node the node the task item is associated with + * @param callback callback to call on success or failure + * @return the handle of the REST call + */ + ServiceCall executeAsync(TaskGroup> taskGroup, DAGNode> node, ServiceCallback callback); +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/package-info.java b/azure-client-runtime/src/main/java/com/microsoft/azure/package-info.java new file mode 100644 index 0000000000000..a94e7e5884c5d --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/package-info.java @@ -0,0 +1,6 @@ +/** + * The package contains the runtime classes required for AutoRest generated + * Azure clients to compile and function. To learn more about AutoRest generator, + * see https://github.com/azure/autorest. + */ +package com.microsoft.azure; \ No newline at end of file diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/serializer/AzureJacksonMapperAdapter.java b/azure-client-runtime/src/main/java/com/microsoft/azure/serializer/AzureJacksonMapperAdapter.java new file mode 100644 index 0000000000000..d73fcd6f925c4 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/serializer/AzureJacksonMapperAdapter.java @@ -0,0 +1,36 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure.serializer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.rest.serializer.FlatteningDeserializer; +import com.microsoft.rest.serializer.FlatteningSerializer; +import com.microsoft.rest.serializer.JacksonMapperAdapter; + +/** + * A serialization helper class overriding {@link JacksonMapperAdapter} with extra + * functionality useful for Azure operations. + */ +public final class AzureJacksonMapperAdapter extends JacksonMapperAdapter { + /** + * An instance of {@link ObjectMapper} to serialize/deserialize objects. + */ + private ObjectMapper azureObjectMapper; + + @Override + public ObjectMapper getObjectMapper() { + if (azureObjectMapper == null) { + azureObjectMapper = new ObjectMapper(); + initializeObjectMapper(azureObjectMapper); + azureObjectMapper.registerModule(FlatteningSerializer.getModule(getSimpleMapper())) + .registerModule(FlatteningDeserializer.getModule(getSimpleMapper())) + .registerModule(CloudErrorDeserializer.getModule(getSimpleMapper())); + } + return azureObjectMapper; + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/serializer/CloudErrorDeserializer.java b/azure-client-runtime/src/main/java/com/microsoft/azure/serializer/CloudErrorDeserializer.java new file mode 100644 index 0000000000000..ee0a371cf2add --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/serializer/CloudErrorDeserializer.java @@ -0,0 +1,65 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure.serializer; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.microsoft.azure.CloudError; + +import java.io.IOException; + +/** + * Custom serializer for serializing {@link CloudError} objects. + */ +public class CloudErrorDeserializer extends JsonDeserializer { + /** Object mapper for default deserializations. */ + private ObjectMapper mapper; + + /** + * Creates an instance of CloudErrorDeserializer. + * + * @param mapper the object mapper for default deserializations. + */ + protected CloudErrorDeserializer(ObjectMapper mapper) { + this.mapper = mapper; + } + + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @param mapper the object mapper for default deserializations. + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule(ObjectMapper mapper) { + SimpleModule module = new SimpleModule(); + module.addDeserializer(CloudError.class, new CloudErrorDeserializer(mapper)); + return module; + } + + @Override + public CloudError deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + JsonNode topNode = p.readValueAsTree(); + if (topNode == null) { + return null; + } + JsonNode errorNode = topNode.get("error"); + if (errorNode == null) { + return null; + } + JsonParser parser = new JsonFactory().createParser(errorNode.toString()); + parser.setCodec(mapper); + return parser.readValueAs(CloudError.class); + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/serializer/package-info.java b/azure-client-runtime/src/main/java/com/microsoft/azure/serializer/package-info.java new file mode 100644 index 0000000000000..f436fafd93926 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/serializer/package-info.java @@ -0,0 +1,5 @@ +/** + * The package contains classes that handle serialization and deserialization + * for the REST call payloads in Azure. + */ +package com.microsoft.azure.serializer; \ No newline at end of file diff --git a/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTest.java b/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTest.java new file mode 100644 index 0000000000000..f3b278797e2dd --- /dev/null +++ b/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTest.java @@ -0,0 +1,145 @@ +package com.microsoft.azure; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class DAGraphTest { + @Test + public void testDAGraphGetNext() { + /** + * |-------->[D]------>[B]-----------[A] + * | ^ ^ + * | | | + * [F]------->[E]-------| | + * | | | + * | |------->[G]----->[C]---- + * | + * |-------->[H]-------------------->[I] + */ + List expectedOrder = new ArrayList<>(); + expectedOrder.add("A"); expectedOrder.add("I"); + expectedOrder.add("B"); expectedOrder.add("C"); expectedOrder.add("H"); + expectedOrder.add("D"); expectedOrder.add("G"); + expectedOrder.add("E"); + expectedOrder.add("F"); + + DAGNode nodeA = new DAGNode<>("A", "dataA"); + DAGNode nodeI = new DAGNode<>("I", "dataI"); + + DAGNode nodeB = new DAGNode<>("B", "dataB"); + nodeB.addDependency(nodeA.key()); + + DAGNode nodeC = new DAGNode<>("C", "dataC"); + nodeC.addDependency(nodeA.key()); + + DAGNode nodeH = new DAGNode<>("H", "dataH"); + nodeH.addDependency(nodeI.key()); + + DAGNode nodeG = new DAGNode<>("G", "dataG"); + nodeG.addDependency(nodeC.key()); + + DAGNode nodeE = new DAGNode<>("E", "dataE"); + nodeE.addDependency(nodeB.key()); + nodeE.addDependency(nodeG.key()); + + DAGNode nodeD = new DAGNode<>("D", "dataD"); + nodeD.addDependency(nodeB.key()); + + + DAGNode nodeF = new DAGNode<>("F", "dataF"); + nodeF.addDependency(nodeD.key()); + nodeF.addDependency(nodeE.key()); + nodeF.addDependency(nodeH.key()); + + DAGraph> dag = new DAGraph<>(nodeF); + dag.addNode(nodeA); + dag.addNode(nodeB); + dag.addNode(nodeC); + dag.addNode(nodeD); + dag.addNode(nodeE); + dag.addNode(nodeG); + dag.addNode(nodeH); + dag.addNode(nodeI); + + dag.prepare(); + DAGNode nextNode = dag.getNext(); + int i = 0; + while (nextNode != null) { + Assert.assertEquals(nextNode.key(), expectedOrder.get(i)); + dag.reportedCompleted(nextNode); + nextNode = dag.getNext(); + i++; + } + + System.out.println("done"); + } + + @Test + public void testGraphMerge() { + /** + * |-------->[D]------>[B]-----------[A] + * | ^ ^ + * | | | + * [F]------->[E]-------| | + * | | | + * | |------->[G]----->[C]---- + * | + * |-------->[H]-------------------->[I] + */ + List expectedOrder = new ArrayList<>(); + expectedOrder.add("A"); expectedOrder.add("I"); + expectedOrder.add("B"); expectedOrder.add("C"); expectedOrder.add("H"); + expectedOrder.add("D"); expectedOrder.add("G"); + expectedOrder.add("E"); + expectedOrder.add("F"); + + DAGraph> graphA = createGraph("A"); + DAGraph> graphI = createGraph("I"); + + DAGraph> graphB = createGraph("B"); + graphA.merge(graphB); + + DAGraph> graphC = createGraph("C"); + graphA.merge(graphC); + + DAGraph> graphH = createGraph("H"); + graphI.merge(graphH); + + DAGraph> graphG = createGraph("G"); + graphC.merge(graphG); + + DAGraph> graphE = createGraph("E"); + graphB.merge(graphE); + graphG.merge(graphE); + + DAGraph> graphD = createGraph("D"); + graphB.merge(graphD); + + DAGraph> graphF = createGraph("F"); + graphD.merge(graphF); + graphE.merge(graphF); + graphH.merge(graphF); + + DAGraph> dag = graphF; + dag.prepare(); + + DAGNode nextNode = dag.getNext(); + int i = 0; + while (nextNode != null) { + Assert.assertEquals(expectedOrder.get(i), nextNode.key()); + // Process the node + dag.reportedCompleted(nextNode); + nextNode = dag.getNext(); + i++; + } + } + + private DAGraph> createGraph(String resourceName) { + DAGNode node = new DAGNode<>(resourceName, "data" + resourceName); + DAGraph> graph = new DAGraph<>(node); + return graph; + } +} diff --git a/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTests.java b/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTests.java new file mode 100644 index 0000000000000..985fe306f0f52 --- /dev/null +++ b/azure-client-runtime/src/test/java/com/microsoft/azure/DAGraphTests.java @@ -0,0 +1,152 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class DAGraphTests { + @Test + public void testDAGraphGetNext() { + /** + * |-------->[D]------>[B]-----------[A] + * | ^ ^ + * | | | + * [F]------->[E]-------| | + * | | | + * | |------->[G]----->[C]---- + * | + * |-------->[H]-------------------->[I] + */ + List expectedOrder = new ArrayList<>(); + expectedOrder.add("A"); expectedOrder.add("I"); + expectedOrder.add("B"); expectedOrder.add("C"); expectedOrder.add("H"); + expectedOrder.add("D"); expectedOrder.add("G"); + expectedOrder.add("E"); + expectedOrder.add("F"); + + DAGNode nodeA = new DAGNode<>("A", "dataA"); + DAGNode nodeI = new DAGNode<>("I", "dataI"); + + DAGNode nodeB = new DAGNode<>("B", "dataB"); + nodeB.addDependency(nodeA.key()); + + DAGNode nodeC = new DAGNode<>("C", "dataC"); + nodeC.addDependency(nodeA.key()); + + DAGNode nodeH = new DAGNode<>("H", "dataH"); + nodeH.addDependency(nodeI.key()); + + DAGNode nodeG = new DAGNode<>("G", "dataG"); + nodeG.addDependency(nodeC.key()); + + DAGNode nodeE = new DAGNode<>("E", "dataE"); + nodeE.addDependency(nodeB.key()); + nodeE.addDependency(nodeG.key()); + + DAGNode nodeD = new DAGNode<>("D", "dataD"); + nodeD.addDependency(nodeB.key()); + + + DAGNode nodeF = new DAGNode<>("F", "dataF"); + nodeF.addDependency(nodeD.key()); + nodeF.addDependency(nodeE.key()); + nodeF.addDependency(nodeH.key()); + + DAGraph> dag = new DAGraph<>(nodeF); + dag.addNode(nodeA); + dag.addNode(nodeB); + dag.addNode(nodeC); + dag.addNode(nodeD); + dag.addNode(nodeE); + dag.addNode(nodeG); + dag.addNode(nodeH); + dag.addNode(nodeI); + + dag.prepare(); + DAGNode nextNode = dag.getNext(); + int i = 0; + while (nextNode != null) { + Assert.assertEquals(nextNode.key(), expectedOrder.get(i)); + dag.reportedCompleted(nextNode); + nextNode = dag.getNext(); + i++; + } + + System.out.println("done"); + } + + @Test + public void testGraphMerge() { + /** + * |-------->[D]------>[B]-----------[A] + * | ^ ^ + * | | | + * [F]------->[E]-------| | + * | | | + * | |------->[G]----->[C]---- + * | + * |-------->[H]-------------------->[I] + */ + List expectedOrder = new ArrayList<>(); + expectedOrder.add("A"); expectedOrder.add("I"); + expectedOrder.add("B"); expectedOrder.add("C"); expectedOrder.add("H"); + expectedOrder.add("D"); expectedOrder.add("G"); + expectedOrder.add("E"); + expectedOrder.add("F"); + + DAGraph> graphA = createGraph("A"); + DAGraph> graphI = createGraph("I"); + + DAGraph> graphB = createGraph("B"); + graphA.merge(graphB); + + DAGraph> graphC = createGraph("C"); + graphA.merge(graphC); + + DAGraph> graphH = createGraph("H"); + graphI.merge(graphH); + + DAGraph> graphG = createGraph("G"); + graphC.merge(graphG); + + DAGraph> graphE = createGraph("E"); + graphB.merge(graphE); + graphG.merge(graphE); + + DAGraph> graphD = createGraph("D"); + graphB.merge(graphD); + + DAGraph> graphF = createGraph("F"); + graphD.merge(graphF); + graphE.merge(graphF); + graphH.merge(graphF); + + DAGraph> dag = graphF; + dag.prepare(); + + DAGNode nextNode = dag.getNext(); + int i = 0; + while (nextNode != null) { + Assert.assertEquals(expectedOrder.get(i), nextNode.key()); + // Process the node + dag.reportedCompleted(nextNode); + nextNode = dag.getNext(); + i++; + } + } + + private DAGraph> createGraph(String resourceName) { + DAGNode node = new DAGNode<>(resourceName, "data" + resourceName); + DAGraph> graph = new DAGraph<>(node); + return graph; + } +} diff --git a/azure-client-runtime/src/test/java/com/microsoft/azure/PagedListTests.java b/azure-client-runtime/src/test/java/com/microsoft/azure/PagedListTests.java new file mode 100644 index 0000000000000..b82c8d0628787 --- /dev/null +++ b/azure-client-runtime/src/test/java/com/microsoft/azure/PagedListTests.java @@ -0,0 +1,127 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class PagedListTests { + private PagedList list; + + @Before + public void setupList() { + list = new PagedList(new TestPage(0, 20)) { + @Override + public Page nextPage(String nextPageLink) throws CloudException, IOException { + int pageNum = Integer.parseInt(nextPageLink); + return new TestPage(pageNum, 20); + } + }; + } + + @Test + public void sizeTest() { + Assert.assertEquals(20, list.size()); + } + + @Test + public void getTest() { + Assert.assertEquals(15, (int) list.get(15)); + } + + @Test + public void iterateTest() { + int j = 0; + for (int i : list) { + Assert.assertEquals(i, j++); + } + } + + @Test + public void removeTest() { + Integer i = list.get(10); + list.remove(10); + Assert.assertEquals(19, list.size()); + Assert.assertEquals(19, (int) list.get(18)); + } + + @Test + public void addTest() { + Integer i = list.get(10); + list.add(100); + Assert.assertEquals(21, list.size()); + Assert.assertEquals(100, (int) list.get(11)); + Assert.assertEquals(19, (int) list.get(20)); + } + + @Test + public void containsTest() { + Assert.assertTrue(list.contains(0)); + Assert.assertTrue(list.contains(3)); + Assert.assertTrue(list.contains(19)); + Assert.assertFalse(list.contains(20)); + } + + @Test + public void containsAllTest() { + List subList = new ArrayList<>(); + subList.addAll(Arrays.asList(0, 3, 19)); + Assert.assertTrue(list.containsAll(subList)); + subList.add(20); + Assert.assertFalse(list.containsAll(subList)); + } + + @Test + public void subListTest() { + List subList = list.subList(5, 15); + Assert.assertEquals(10, subList.size()); + Assert.assertTrue(list.containsAll(subList)); + Assert.assertEquals(7, (int) subList.get(2)); + } + + @Test + public void testIndexOf() { + Assert.assertEquals(15, list.indexOf(15)); + } + + @Test + public void testLastIndexOf() { + Assert.assertEquals(15, list.lastIndexOf(15)); + } + + public static class TestPage implements Page { + private int page; + private int max; + + public TestPage(int page, int max) { + this.page = page; + this.max = max; + } + + @Override + public String getNextPageLink() { + if (page + 1 == max) { + return null; + } + return Integer.toString(page + 1); + } + + @Override + public List getItems() { + List items = new ArrayList<>(); + items.add(page); + return items; + } + } +} diff --git a/azure-client-runtime/src/test/java/com/microsoft/azure/RequestIdHeaderInterceptorTests.java b/azure-client-runtime/src/test/java/com/microsoft/azure/RequestIdHeaderInterceptorTests.java new file mode 100644 index 0000000000000..060336cd23d29 --- /dev/null +++ b/azure-client-runtime/src/test/java/com/microsoft/azure/RequestIdHeaderInterceptorTests.java @@ -0,0 +1,91 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.azure; + +import com.microsoft.rest.retry.RetryHandler; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; + +public class RequestIdHeaderInterceptorTests { + private static final String REQUEST_ID_HEADER = "x-ms-client-request-id"; + + @Test + public void newRequestIdForEachCall() throws Exception { + RestClient restClient = new RestClient.Builder() + .withBaseUrl("http://localhost") + .withInterceptor(new RequestIdHeaderInterceptor()) + .withInterceptor(new Interceptor() { + private String firstRequestId = null; + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + if (request.header(REQUEST_ID_HEADER) != null) { + if (firstRequestId == null) { + firstRequestId = request.header(REQUEST_ID_HEADER); + return new Response.Builder().code(200).request(request) + .protocol(Protocol.HTTP_1_1).build(); + } else if (!request.header(REQUEST_ID_HEADER).equals(firstRequestId)) { + return new Response.Builder().code(200).request(request) + .protocol(Protocol.HTTP_1_1).build(); + } + } + return new Response.Builder().code(400).request(request) + .protocol(Protocol.HTTP_1_1).build(); + } + }) + .build(); + AzureServiceClient serviceClient = new AzureServiceClient(restClient) { }; + Response response = serviceClient.restClient().httpClient() + .newCall(new Request.Builder().get().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + response = serviceClient.restClient().httpClient() + .newCall(new Request.Builder().get().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } + + @Test + public void sameRequestIdForRetry() throws Exception { + RestClient restClient = new RestClient.Builder() + .withBaseUrl("http://localhost") + .withInterceptor(new RequestIdHeaderInterceptor()) + .withInterceptor(new RetryHandler()) + .withInterceptor(new Interceptor() { + private String firstRequestId = null; + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + if (request.header(REQUEST_ID_HEADER) != null) { + if (firstRequestId == null) { + firstRequestId = request.header(REQUEST_ID_HEADER); + return new Response.Builder().code(500).request(request) + .protocol(Protocol.HTTP_1_1).build(); + } else if (request.header(REQUEST_ID_HEADER).equals(firstRequestId)) { + return new Response.Builder().code(200).request(request) + .protocol(Protocol.HTTP_1_1).build(); + } + } + return new Response.Builder().code(400).request(request) + .protocol(Protocol.HTTP_1_1).build(); + } + }) + .build(); + AzureServiceClient serviceClient = new AzureServiceClient(restClient) { }; + Response response = serviceClient.restClient().httpClient() + .newCall(new Request.Builder().get().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } +} diff --git a/build-tools/pom.xml b/build-tools/pom.xml new file mode 100644 index 0000000000000..d540f8f5e6980 --- /dev/null +++ b/build-tools/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + com.microsoft.azure + autorest-clientruntime-for-java + 1.0.0-SNAPSHOT + ../pom.xml + + + autorest-build-tools + jar + + Build tools for AutoRest client runtime for Java + This package contains the build tools for AutoRest generated Java clients. + https://github.com/Azure/autorest-clientruntime-for-java + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + + scm:git:https://github.com/Azure/autorest-clientruntime-for-java + scm:git:git@github.com:Azure/autorest-clientruntime-for-java.git + HEAD + + + + UTF-8 + + + + + + microsoft + Microsoft + + + diff --git a/build-tools/src/main/resources/checkstyle.xml b/build-tools/src/main/resources/checkstyle.xml new file mode 100644 index 0000000000000..1875d6f100cab --- /dev/null +++ b/build-tools/src/main/resources/checkstyle.xml @@ -0,0 +1,255 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build-tools/src/main/resources/suppressions.xml b/build-tools/src/main/resources/suppressions.xml new file mode 100644 index 0000000000000..29e5bd66eefb5 --- /dev/null +++ b/build-tools/src/main/resources/suppressions.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/client-runtime/build.gradle b/client-runtime/build.gradle new file mode 100644 index 0000000000000..e530072694672 --- /dev/null +++ b/client-runtime/build.gradle @@ -0,0 +1,107 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.2' + } +} + +apply plugin: 'java' +apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'checkstyle' + +group = 'com.microsoft.rest' +version = '1.0.0-SNAPSHOT' + +checkstyle { + toolVersion = "6.18" + configFile = new File("$rootDir/src/client/Java/build-tools/src/main/resources/checkstyle.xml") + configProperties = [samedir: "$rootDir/src/client/Java/build-tools/src/main/resources"] + reportsDir = new File("$rootDir/src/client/Java/build-tools/reports") +} + +dependencies { + compile 'com.google.guava:guava:18.0' + compile 'com.squareup.retrofit2:retrofit:2.0.2' + compile 'com.squareup.okhttp3:okhttp:3.3.1' + compile 'com.squareup.okhttp3:logging-interceptor:3.3.1' + compile 'com.squareup.okhttp3:okhttp-urlconnection:3.3.1' + compile 'com.squareup.retrofit2:converter-jackson:2.0.2' + compile 'com.fasterxml.jackson.datatype:jackson-datatype-joda:2.7.2' + compile 'org.apache.commons:commons-lang3:3.4' + testCompile 'junit:junit:4.12' + testCompile 'junit:junit-dep:4.11' + deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" +} + +uploadArchives { + repositories { + mavenDeployer { + configuration = configurations.deployerJars + snapshotRepository(url: "ftp://waws-prod-bay-005.ftp.azurewebsites.windows.net/site/wwwroot/") { + authentication(userName: username, password: password) + } + repository(url: "file://$buildDir/repository") + pom.project { + name 'Microsoft AutoRest Runtime for Java' + description 'This is the client runtime for AutoRest generated Java clients.' + url 'https://github.com/Azure/autorest' + + scm { + url 'scm:git:https://github.com/Azure/AutoRest' + connection 'scm:git:git://github.com/Azure/AutoRest.git' + } + + licenses { + license { + name 'The MIT License (MIT)' + url 'http://opensource.org/licenses/MIT' + distribution 'repo' + } + } + + developers { + developer { + id 'microsoft' + name 'Microsoft' + } + } + } + } + } +} + +test { + testLogging { + events "passed", "skipped", "failed", "standardError" + } +} + +javadoc { + options.encoding = 'UTF-8' +} + +task sourcesJar(type: Jar, dependsOn:classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: [javadoc]) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar + archives javadocJar +} + +test { + reports.getHtml() + reports.html.destination = file("$rootDir/TestResults/JavaRuntime") +} + +tasks.compileJava.dependsOn 'clean' + +println sourceSets.main.runtimeClasspath.asPath diff --git a/client-runtime/pom.xml b/client-runtime/pom.xml new file mode 100644 index 0000000000000..e63a421434b38 --- /dev/null +++ b/client-runtime/pom.xml @@ -0,0 +1,121 @@ + + + 4.0.0 + + com.microsoft.azure + autorest-clientruntime-for-java + 1.0.0-SNAPSHOT + ../pom.xml + + + com.microsoft.rest + client-runtime + jar + + Java Client Runtime for AutoRest + This package contains the basic runtime for AutoRest generated Java clients. + https://github.com/Azure/autorest-clientruntime-for-java + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + + scm:git:https://github.com/Azure/autorest-clientruntime-for-java + scm:git:git@github.com:Azure/autorest-clientruntime-for-java.git + HEAD + + + + UTF-8 + + + + + + microsoft + Microsoft + + + + + + com.google.guava + guava + + + com.squareup.retrofit2 + retrofit + + + com.squareup.okhttp3 + okhttp + + + com.squareup.okhttp3 + logging-interceptor + + + com.squareup.okhttp3 + okhttp-urlconnection + + + com.squareup.retrofit2 + converter-jackson + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + + + org.apache.commons + commons-lang3 + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.8 + + *.implementation.*;*.utils.*;com.microsoft.schemas._2003._10.serialization;*.blob.core.storage + /** +
* Copyright (c) Microsoft Corporation. All rights reserved. +
* Licensed under the MIT License. See License.txt in the project root for +
* license information. +
*/]]>
+
+
+ +
+
+
diff --git a/client-runtime/src/main/java/com/microsoft/rest/BaseUrlHandler.java b/client-runtime/src/main/java/com/microsoft/rest/BaseUrlHandler.java new file mode 100644 index 0000000000000..46eacbd472e23 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/BaseUrlHandler.java @@ -0,0 +1,45 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import java.io.IOException; + +import okhttp3.HttpUrl; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Handles dynamic replacements on base URL. The arguments must be in pairs + * with the string in raw URL to replace as replacements[i] and the dynamic + * part as replacements[i+1]. E.g. {subdomain}.microsoft.com can be set + * dynamically by setting header x-ms-parameterized-host: "{subdomain}, azure" + */ +public class BaseUrlHandler implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + String parameters = request.header("x-ms-parameterized-host"); + if (parameters != null && !parameters.isEmpty()) { + String[] replacements = parameters.split(", "); + if (replacements.length % 2 != 0) { + throw new IllegalArgumentException("Must provide a replacement value for each pattern"); + } + String baseUrl = request.url().toString(); + for (int i = 0; i < replacements.length; i += 2) { + baseUrl = baseUrl.replaceAll("(?i)\\Q" + replacements[i] + "\\E", replacements[i + 1]); + } + HttpUrl baseHttpUrl = HttpUrl.parse(baseUrl); + request = request.newBuilder() + .url(baseHttpUrl) + .removeHeader("x-ms-parameterized-host") + .build(); + } + return chain.proceed(request); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/CustomHeadersInterceptor.java b/client-runtime/src/main/java/com/microsoft/rest/CustomHeadersInterceptor.java new file mode 100644 index 0000000000000..00903b93a9e01 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/CustomHeadersInterceptor.java @@ -0,0 +1,136 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An instance of this class enables adding custom headers in client requests + * when added to the {@link okhttp3.OkHttpClient} interceptors. + */ +public class CustomHeadersInterceptor implements Interceptor { + /** + * A mapping of custom headers. + */ + private Map> headers; + + /** + * Initialize an instance of {@link CustomHeadersInterceptor} class. + */ + public CustomHeadersInterceptor() { + headers = new HashMap>(); + } + + /** + * Initialize an instance of {@link CustomHeadersInterceptor} class. + * + * @param key the key for the header + * @param value the value of the header + */ + public CustomHeadersInterceptor(String key, String value) { + this(); + addHeader(key, value); + } + + /** + * Add a single header key-value pair. If one with the name already exists, + * it gets replaced. + * + * @param name the name of the header. + * @param value the value of the header. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor replaceHeader(String name, String value) { + this.headers.put(name, new ArrayList()); + this.headers.get(name).add(value); + return this; + } + + /** + * Add a single header key-value pair. If one with the name already exists, + * both stay in the header map. + * + * @param name the name of the header. + * @param value the value of the header. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor addHeader(String name, String value) { + if (!this.headers.containsKey(name)) { + this.headers.put(name, new ArrayList()); + } + this.headers.get(name).add(value); + return this; + } + + /** + * Add all headers in a {@link Headers} object. + * + * @param headers an OkHttp {@link Headers} object. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor addHeaders(Headers headers) { + this.headers.putAll(headers.toMultimap()); + return this; + } + + /** + * Add all headers in a header map. + * + * @param headers a map of headers. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor addHeaderMap(Map headers) { + for (Map.Entry header : headers.entrySet()) { + this.headers.put(header.getKey(), Collections.singletonList(header.getValue())); + } + return this; + } + + /** + * Add all headers in a header multimap. + * + * @param headers a multimap of headers. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor addHeaderMultimap(Map> headers) { + this.headers.putAll(headers); + return this; + } + + /** + * Remove a header. + * + * @param name the name of the header to remove. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor removeHeader(String name) { + this.headers.remove(name); + return this; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request.Builder builder = chain.request().newBuilder(); + for (Map.Entry> header : headers.entrySet()) { + for (String value : header.getValue()) { + builder = builder.header(header.getKey(), value); + } + } + return chain.proceed(builder.build()); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/DateTimeRfc1123.java b/client-runtime/src/main/java/com/microsoft/rest/DateTimeRfc1123.java new file mode 100644 index 0000000000000..d4e8078cb7173 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/DateTimeRfc1123.java @@ -0,0 +1,80 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import java.util.Locale; + +/** + * Simple wrapper over joda.time.DateTime used for specifying RFC1123 format during serialization/deserialization. + */ +public class DateTimeRfc1123 { + /** + * The pattern of the datetime used for RFC1123 datetime format. + */ + private static final DateTimeFormatter RFC1123_DATE_TIME_FORMATTER = + DateTimeFormat.forPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'").withZoneUTC().withLocale(Locale.US); + + /** + * The actual datetime object. + */ + private final DateTime dateTime; + + /** + * Creates a new DateTimeRfc1123 object with the specified DateTime. + * @param dateTime The DateTime object to wrap. + */ + public DateTimeRfc1123(DateTime dateTime) { + this.dateTime = dateTime; + } + + /** + * Creates a new DateTimeRfc1123 object with the specified DateTime. + * @param formattedString The datetime string in RFC1123 format + */ + public DateTimeRfc1123(String formattedString) { + this.dateTime = DateTime.parse(formattedString, RFC1123_DATE_TIME_FORMATTER); + } + + /** + * Returns the underlying DateTime. + * @return The underlying DateTime. + */ + public DateTime getDateTime() { + if (this.dateTime == null) { + return null; + } + return this.dateTime; + } + + @Override + public String toString() { + return RFC1123_DATE_TIME_FORMATTER.print(this.dateTime); + } + + @Override + public int hashCode() { + return this.dateTime.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (!(obj instanceof DateTimeRfc1123)) { + return false; + } + + DateTimeRfc1123 rhs = (DateTimeRfc1123) obj; + return this.dateTime.equals(rhs.getDateTime()); + } +} \ No newline at end of file diff --git a/client-runtime/src/main/java/com/microsoft/rest/RestException.java b/client-runtime/src/main/java/com/microsoft/rest/RestException.java new file mode 100644 index 0000000000000..cf5dff556aea0 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/RestException.java @@ -0,0 +1,46 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +/** + * Exception thrown for an invalid response with custom error information. + */ +public abstract class RestException extends Exception { + /** + * Initializes a new instance of the AutoRestException class. + */ + public RestException() { } + + /** + * Initializes a new instance of the AutoRestException class. + * + * @param message The exception message. + */ + public RestException(String message) { + super(message); + } + + /** + * Initializes a new instance of the AutoRestException class. + * + * @param cause exception that caused this exception to occur + */ + public RestException(Throwable cause) { + super(cause); + } + + /** + * Initializes a new instance of the AutoRestException class. + * + * @param message the exception message + * @param cause exception that caused this exception to occur + */ + public RestException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/ServiceCall.java b/client-runtime/src/main/java/com/microsoft/rest/ServiceCall.java new file mode 100644 index 0000000000000..dcffd781c7c59 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/ServiceCall.java @@ -0,0 +1,65 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import retrofit2.Call; + +/** + * An instance of this class provides access to the underlying REST call invocation. + * This class wraps around the Retrofit Call object and allows updates to it in the + * progress of a long running operation or a paging operation. + */ +public class ServiceCall { + /** + * The Retrofit method invocation. + */ + private Call call; + + /** + * Creates an instance of ServiceCall. + * + * @param call the Retrofit call to wrap around. + */ + public ServiceCall(Call call) { + this.call = call; + } + + /** + * Updates the current Retrofit call object. + * + * @param call the new call object. + */ + public void newCall(Call call) { + this.call = call; + } + + /** + * Gets the current Retrofit call object. + * + * @return the current call object. + */ + public Call getCall() { + return call; + } + + /** + * Cancel the Retrofit call if possible. + */ + public void cancel() { + call.cancel(); + } + + /** + * If the Retrofit call has been canceled. + * + * @return true if the call has been canceled; false otherwise. + */ + public boolean isCanceled() { + return call.isCanceled(); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/ServiceCallback.java b/client-runtime/src/main/java/com/microsoft/rest/ServiceCallback.java new file mode 100644 index 0000000000000..dd3791bbb2d19 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/ServiceCallback.java @@ -0,0 +1,29 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +/** + * The callback used for client side asynchronous operations. + * + * @param the type of the response + */ +public abstract class ServiceCallback { + /** + * Override this method to handle REST call failures. + * + * @param t the exception thrown from the pipeline. + */ + public abstract void failure(Throwable t); + + /** + * Override this method to handle successful REST call results. + * + * @param result the ServiceResponse holding the response. + */ + public abstract void success(ServiceResponse result); +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/ServiceClient.java b/client-runtime/src/main/java/com/microsoft/rest/ServiceClient.java new file mode 100644 index 0000000000000..8311a243de139 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/ServiceClient.java @@ -0,0 +1,88 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import com.microsoft.rest.retry.RetryHandler; +import com.microsoft.rest.serializer.JacksonMapperAdapter; + +import java.net.CookieManager; +import java.net.CookiePolicy; + +import okhttp3.JavaNetCookieJar; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +/** + * ServiceClient is the abstraction for accessing REST operations and their payload data types. + */ +public abstract class ServiceClient { + /** The HTTP client. */ + private OkHttpClient httpClient; + /** The Retrofit instance. */ + private Retrofit retrofit; + /** The adapter to a Jackson {@link com.fasterxml.jackson.databind.ObjectMapper}. */ + private JacksonMapperAdapter mapperAdapter; + + /** + * Initializes a new instance of the ServiceClient class. + * + * @param baseUrl the service endpoint + */ + protected ServiceClient(String baseUrl) { + this(baseUrl, new OkHttpClient.Builder(), new Retrofit.Builder()); + } + + /** + * Initializes a new instance of the ServiceClient class. + * + */ + protected ServiceClient(String baseUrl, OkHttpClient.Builder clientBuilder, Retrofit.Builder restBuilder) { + if (clientBuilder == null) { + throw new IllegalArgumentException("clientBuilder == null"); + } + if (restBuilder == null) { + throw new IllegalArgumentException("restBuilder == null"); + } + this.mapperAdapter = new JacksonMapperAdapter(); + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); + this.httpClient = clientBuilder + .cookieJar(new JavaNetCookieJar(cookieManager)) + .addInterceptor(new UserAgentInterceptor()) + .addInterceptor(new BaseUrlHandler()) + .addInterceptor(new CustomHeadersInterceptor()) + .addInterceptor(new RetryHandler()) + .build(); + this.retrofit = restBuilder + .baseUrl(baseUrl) + .client(httpClient) + .addConverterFactory(mapperAdapter.getConverterFactory()) + .build(); + } + + /** + * @return the Retrofit instance. + */ + public Retrofit retrofit() { + return this.retrofit; + } + + /** + * @return the HTTP client. + */ + public OkHttpClient httpClient() { + return this.httpClient; + } + + /** + * @return the adapter to a Jackson {@link com.fasterxml.jackson.databind.ObjectMapper}. + */ + public JacksonMapperAdapter mapperAdapter() { + return this.mapperAdapter; + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/ServiceException.java b/client-runtime/src/main/java/com/microsoft/rest/ServiceException.java new file mode 100644 index 0000000000000..c041ae3bda3ab --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/ServiceException.java @@ -0,0 +1,94 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import retrofit2.Response; + +/** + * Exception thrown for an invalid response with custom error information. + */ +public class ServiceException extends RestException { + /** + * Information about the associated HTTP response. + */ + private Response response; + + /** + * The HTTP response body. + */ + private Object body; + + /** + * Initializes a new instance of the ServiceException class. + */ + public ServiceException() { } + + /** + * Initializes a new instance of the ServiceException class. + * + * @param message The exception message. + */ + public ServiceException(final String message) { + super(message); + } + + /** + * Initializes a new instance of the ServiceException class. + * + * @param message the exception message + * @param cause exception that caused this exception to occur + */ + public ServiceException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Initializes a new instance of the ServiceException class. + * + * @param cause exception that caused this exception to occur + */ + public ServiceException(final Throwable cause) { + super(cause); + } + + /** + * Gets information about the associated HTTP response. + * + * @return the HTTP response + */ + public Response getResponse() { + return response; + } + + /** + * Gets the HTTP response body. + * + * @return the response body + */ + public Object getBody() { + return body; + } + + /** + * Sets the HTTP response. + * + * @param response the HTTP response + */ + public void setResponse(Response response) { + this.response = response; + } + + /** + * Sets the HTTP response body. + * + * @param body the response object + */ + public void setBody(Object body) { + this.body = body; + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/ServiceResponse.java b/client-runtime/src/main/java/com/microsoft/rest/ServiceResponse.java new file mode 100644 index 0000000000000..a116349a694dc --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/ServiceResponse.java @@ -0,0 +1,88 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import okhttp3.ResponseBody; +import retrofit2.Response; + +/** + * An instance of this class holds a response object and a raw REST response. + * + * @param the type of the response + */ +public class ServiceResponse { + /** + * The response body object. + */ + private T body; + + /** + * The retrofit response wrapper containing information about the REST response. + */ + private Response response; + + /** + * The retrofit response wrapper if it's returned from a HEAD operation. + */ + private Response headResponse; + + /** + * Instantiate a ServiceResponse instance with a response object and a raw REST response. + * + * @param body deserialized response object + * @param response raw REST response + */ + public ServiceResponse(T body, Response response) { + this.body = body; + this.response = response; + } + + /** + * Instantiate a ServiceResponse instance with a response from a HEAD operation. + * + * @param headResponse raw REST response from a HEAD operation + */ + public ServiceResponse(Response headResponse) { + this.headResponse = headResponse; + } + + /** + * Gets the response object. + * @return the response object. Null if there isn't one. + */ + public T getBody() { + return this.body; + } + + /** + * Sets the response object. + * + * @param body the response object. + */ + public void setBody(T body) { + this.body = body; + } + + /** + * Gets the raw REST response. + * + * @return the raw REST response. + */ + public Response getResponse() { + return response; + } + + /** + * Gets the raw REST response from a HEAD operation. + * + * @return the raw REST response from a HEAD operation. + */ + public Response getHeadResponse() { + return headResponse; + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseBuilder.java b/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseBuilder.java new file mode 100644 index 0000000000000..3ad57cb938f40 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseBuilder.java @@ -0,0 +1,289 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.microsoft.rest.serializer.JacksonMapperAdapter; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +import okhttp3.ResponseBody; +import retrofit2.Response; + +/** + * The builder for building a {@link ServiceResponse}. + * + * @param The return type the caller expects from the REST response. + * @param the exception to throw in case of error. + */ +public class ServiceResponseBuilder { + /** + * A mapping of HTTP status codes and their corresponding return types. + */ + protected Map responseTypes; + + /** + * The exception type to thrown in case of error. + */ + protected Class exceptionType; + + /** + * The mapperAdapter used for deserializing the response. + */ + protected JacksonMapperAdapter mapperAdapter; + + /** + * Create a ServiceResponseBuilder instance. + * + * @param mapperAdapter the serialization utils to use for deserialization operations + */ + public ServiceResponseBuilder(JacksonMapperAdapter mapperAdapter) { + this(mapperAdapter, new HashMap()); + } + + /** + * Create a ServiceResponseBuilder instance. + * + * @param mapperAdapter the serialization utils to use for deserialization operations + * @param responseTypes a mapping of response status codes and response destination types + */ + public ServiceResponseBuilder(JacksonMapperAdapter mapperAdapter, Map responseTypes) { + this.mapperAdapter = mapperAdapter; + this.responseTypes = responseTypes; + this.exceptionType = ServiceException.class; + this.responseTypes.put(0, Object.class); + } + + /** + * Register a mapping from a response status code to a response destination type. + * + * @param statusCode the status code. + * @param type the type to deserialize. + * @return the same builder instance. + */ + public ServiceResponseBuilder register(int statusCode, final Type type) { + this.responseTypes.put(statusCode, type); + return this; + } + + /** + * Register a destination type for errors with models. + * + * @param type the type to deserialize. + * @return the same builder instance. + */ + public ServiceResponseBuilder registerError(final Class type) { + this.exceptionType = type; + try { + Field f = type.getDeclaredField("body"); + this.responseTypes.put(0, f.getType()); + } catch (NoSuchFieldException e) { + // AutoRestException always has a body. Register Object as a fallback plan. + this.responseTypes.put(0, Object.class); + } + return this; + } + + /** + * Register all the mappings from a response status code to a response + * destination type stored in a {@link Map}. + * + * @param responseTypes the mapping from response status codes to response types. + * @return the same builder instance. + */ + public ServiceResponseBuilder registerAll(Map responseTypes) { + this.responseTypes.putAll(responseTypes); + return this; + } + + /** + * Build a ServiceResponse instance from a REST call response and a + * possible error. + * + *

+ * If the status code in the response is registered, the response will + * be considered valid and deserialized into the specified destination + * type. If the status code is not registered, the response will be + * considered invalid and deserialized into the specified error type if + * there is one. An AutoRestException is also thrown. + *

+ * + * @param response the {@link Response} instance from REST call + * @return a ServiceResponse instance of generic type {@link T} + * @throws E exceptions from the REST call + * @throws IOException exceptions from deserialization + */ + @SuppressWarnings("unchecked") + public ServiceResponse build(Response response) throws E, IOException { + if (response == null) { + return null; + } + + int statusCode = response.code(); + ResponseBody responseBody; + if (response.isSuccessful()) { + responseBody = response.body(); + } else { + responseBody = response.errorBody(); + } + + if (responseTypes.containsKey(statusCode)) { + return new ServiceResponse<>((T) buildBody(statusCode, responseBody), response); + } else if (response.isSuccessful() && responseTypes.size() == 1) { + return new ServiceResponse<>((T) buildBody(statusCode, responseBody), response); + } else { + try { + E exception = (E) exceptionType.getConstructor(String.class).newInstance("Invalid status code " + statusCode); + exceptionType.getMethod("setResponse", response.getClass()).invoke(exception, response); + exceptionType.getMethod("setBody", (Class) responseTypes.get(0)).invoke(exception, buildBody(statusCode, responseBody)); + throw exception; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IOException("Invalid status code " + statusCode + ", but an instance of " + exceptionType.getCanonicalName() + + " cannot be created.", e); + } + } + } + + /** + * Build a ServiceResponse instance from a REST call response and a + * possible error, which does not have a response body. + * + *

+ * If the status code in the response is registered, the response will + * be considered valid. If the status code is not registered, the + * response will be considered invalid. An AutoRestException is also thrown. + *

+ * + * @param response the {@link Response} instance from REST call + * @return a ServiceResponse instance of generic type {@link T} + * @throws E exceptions from the REST call + * @throws IOException exceptions from deserialization + */ + @SuppressWarnings("unchecked") + public ServiceResponse buildEmpty(Response response) throws E, IOException { + int statusCode = response.code(); + if (responseTypes.containsKey(statusCode)) { + return new ServiceResponse<>(response); + } else if (response.isSuccessful() && responseTypes.size() == 1) { + return new ServiceResponse<>(response); + } else { + try { + E exception = (E) exceptionType.getConstructor(String.class).newInstance("Invalid status code " + statusCode); + exceptionType.getMethod("setResponse", response.getClass()).invoke(exception, response); + response.errorBody().close(); + throw exception; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IOException("Invalid status code " + statusCode + ", but an instance of " + exceptionType.getCanonicalName() + + " cannot be created.", e); + } + } + } + + /** + * Build a ServiceResponseWithHeaders instance from a REST call response, a header + * in JSON format, and a possible error. + * + *

+ * If the status code in the response is registered, the response will + * be considered valid and deserialized into the specified destination + * type. If the status code is not registered, the response will be + * considered invalid and deserialized into the specified error type if + * there is one. An AutoRestException is also thrown. + *

+ * + * @param response the {@link Response} instance from REST call + * @param headerType the type of the header + * @param the type of the header + * @return a ServiceResponseWithHeaders instance of generic type {@link T} + * @throws E exceptions from the REST call + * @throws IOException exceptions from deserialization + */ + public ServiceResponseWithHeaders buildWithHeaders(Response response, Class headerType) throws E, IOException { + ServiceResponse bodyResponse = build(response); + THeader headers = mapperAdapter.deserialize( + mapperAdapter.serialize(response.headers()), + headerType); + return new ServiceResponseWithHeaders<>(bodyResponse.getBody(), headers, bodyResponse.getResponse()); + } + + /** + * Build a ServiceResponseWithHeaders instance from a REST call response, a header + * in JSON format, and a possible error, which does not have a response body. + * + *

+ * If the status code in the response is registered, the response will + * be considered valid. If the status code is not registered, the + * response will be considered invalid. An AutoRestException is also thrown. + *

+ * + * @param response the {@link Response} instance from REST call + * @param headerType the type of the header + * @param the type of the header + * @return a ServiceResponseWithHeaders instance of generic type {@link T} + * @throws E exceptions from the REST call + * @throws IOException exceptions from deserialization + */ + public ServiceResponseWithHeaders buildEmptyWithHeaders(Response response, Class headerType) throws E, IOException { + ServiceResponse bodyResponse = buildEmpty(response); + THeader headers = mapperAdapter.deserialize( + mapperAdapter.serialize(response.headers()), + headerType); + ServiceResponseWithHeaders serviceResponse = new ServiceResponseWithHeaders<>(headers, bodyResponse.getHeadResponse()); + serviceResponse.setBody(bodyResponse.getBody()); + return serviceResponse; + } + + /** + * Builds the body object from the HTTP status code and returned response + * body undeserialized and wrapped in {@link ResponseBody}. + * + * @param statusCode the HTTP status code + * @param responseBody the response body + * @return the response body, deserialized + * @throws IOException thrown for any deserialization errors + */ + protected Object buildBody(int statusCode, ResponseBody responseBody) throws IOException { + if (responseBody == null) { + return null; + } + + Type type; + if (responseTypes.containsKey(statusCode)) { + type = responseTypes.get(statusCode); + } else if (responseTypes.get(0) != Object.class) { + type = responseTypes.get(0); + } else { + type = new TypeReference() { }.getType(); + } + + // Void response + if (type == Void.class) { + return null; + } + // Return raw response if InputStream is the target type + else if (type == InputStream.class) { + InputStream stream = responseBody.byteStream(); + return stream; + } + // Deserialize + else { + String responseContent = responseBody.string(); + responseBody.close(); + if (responseContent.length() <= 0) { + return null; + } + return mapperAdapter.deserialize(responseContent, type); + } + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseCallback.java b/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseCallback.java new file mode 100644 index 0000000000000..8808bfac9639c --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseCallback.java @@ -0,0 +1,39 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; + +/** + * Inner callback used to merge both successful and failed responses into one + * callback for customized response handling in a response handling delegate. + * + * @param the response body type + */ +public abstract class ServiceResponseCallback implements Callback { + /** + * The client callback. + */ + private ServiceCallback serviceCallback; + + /** + * Creates an instance of ServiceResponseCallback. + * + * @param serviceCallback the client callback to call on a terminal state. + */ + public ServiceResponseCallback(ServiceCallback serviceCallback) { + this.serviceCallback = serviceCallback; + } + + @Override + public void onFailure(Call call, Throwable t) { + serviceCallback.failure(new ServiceException(t)); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseEmptyCallback.java b/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseEmptyCallback.java new file mode 100644 index 0000000000000..fe848128c57df --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseEmptyCallback.java @@ -0,0 +1,38 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import retrofit2.Call; +import retrofit2.Callback; + +/** + * Inner callback used to merge both successful and failed responses into one + * callback for customized response handling in a response handling delegate. + * + * @param the response body type + */ +public abstract class ServiceResponseEmptyCallback implements Callback { + /** + * The client callback. + */ + private ServiceCallback serviceCallback; + + /** + * Creates an instance of ServiceResponseCallback. + * + * @param serviceCallback the client callback to call on a terminal state. + */ + public ServiceResponseEmptyCallback(ServiceCallback serviceCallback) { + this.serviceCallback = serviceCallback; + } + + @Override + public void onFailure(Call call, Throwable t) { + serviceCallback.failure(new ServiceException(t)); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseWithHeaders.java b/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseWithHeaders.java new file mode 100644 index 0000000000000..a09d119599bf5 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/ServiceResponseWithHeaders.java @@ -0,0 +1,55 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import okhttp3.ResponseBody; +import retrofit2.Response; + +/** + * An instance of this class holds a response object and a raw REST response. + * + * @param the type of the response + * @param the type of the response header object + */ +public class ServiceResponseWithHeaders extends ServiceResponse { + /** + * The response header headers. + */ + private THeader headers; + + /** + * Instantiate a ServiceResponse instance with a response object and a raw REST response. + * + * @param body deserialized response object + * @param headers deserialized response header object + * @param response raw REST response + */ + public ServiceResponseWithHeaders(TBody body, THeader headers, Response response) { + super(body, response); + this.headers = headers; + } + + /** + * Instantiate a ServiceResponse instance with a response object and a raw REST response. + * + * @param headers deserialized response header object + * @param response raw REST response + */ + public ServiceResponseWithHeaders(THeader headers, Response response) { + super(response); + this.headers = headers; + } + + /** + * Gets the response headers. + * @return the response headers. Null if there isn't one. + */ + public THeader getHeaders() { + return this.headers; + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/UserAgentInterceptor.java b/client-runtime/src/main/java/com/microsoft/rest/UserAgentInterceptor.java new file mode 100644 index 0000000000000..7b822078d79ab --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/UserAgentInterceptor.java @@ -0,0 +1,79 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +/** + * User agent interceptor for putting a 'User-Agent' header in the request. + */ +public class UserAgentInterceptor implements Interceptor { + /** + * The default user agent header. + */ + private static final String DEFAULT_USER_AGENT_HEADER = "AutoRest-Java"; + + /** + * The user agent header string. + */ + private String userAgent; + + /** + * Initialize an instance of {@link UserAgentInterceptor} class with the default + * 'User-Agent' header. + */ + public UserAgentInterceptor() { + this.userAgent = DEFAULT_USER_AGENT_HEADER; + } + + /** + * Overwrite the User-Agent header. + * + * @param userAgent the new user agent value. + * @return the user agent interceptor itself + */ + public UserAgentInterceptor withUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + /** + * Append a text to the User-Agent header. + * + * @param userAgent the user agent value to append. + * @return the user agent interceptor itself + */ + public UserAgentInterceptor appendUserAgent(String userAgent) { + this.userAgent += " " + userAgent; + return this; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + String header = request.header("User-Agent"); + if (header == null) { + header = DEFAULT_USER_AGENT_HEADER; + } + if (!userAgent.equals(DEFAULT_USER_AGENT_HEADER)) { + if (header.equals(DEFAULT_USER_AGENT_HEADER)) { + header = userAgent; + } else { + header = userAgent + " " + header; + } + } + request = chain.request().newBuilder() + .header("User-Agent", header) + .build(); + return chain.proceed(request); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/Validator.java b/client-runtime/src/main/java/com/microsoft/rest/Validator.java new file mode 100644 index 0000000000000..51c24722404af --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/Validator.java @@ -0,0 +1,124 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; + +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.joda.time.Period; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; + +/** + * Validates user provided parameters are not null if they are required. + */ +public final class Validator { + /** + * Hidden constructor for utility class. + */ + private Validator() { } + + /** + * Validates a user provided required parameter to be not null. + * An {@link IllegalArgumentException} is thrown if a property fails the validation. + * + * @param parameter the parameter to validate + * @throws IllegalArgumentException thrown when the Validator determines the argument is invalid + */ + public static void validate(Object parameter) throws IllegalArgumentException { + // Validation of top level payload is done outside + if (parameter == null) { + return; + } + + Class parameterType = parameter.getClass(); + TypeToken parameterToken = TypeToken.of(parameterType); + if (Primitives.isWrapperType(parameterType)) { + parameterToken = parameterToken.unwrap(); + } + if (parameterToken.isPrimitive() + || parameterType.isEnum() + || parameterToken.isAssignableFrom(LocalDate.class) + || parameterToken.isAssignableFrom(DateTime.class) + || parameterToken.isAssignableFrom(String.class) + || parameterToken.isAssignableFrom(DateTimeRfc1123.class) + || parameterToken.isAssignableFrom(Period.class)) { + return; + } + + for (Class c : parameterToken.getTypes().classes().rawTypes()) { + // Ignore checks for Object type. + if (c.isAssignableFrom(Object.class)) { + continue; + } + for (Field field : c.getDeclaredFields()) { + field.setAccessible(true); + JsonProperty annotation = field.getAnnotation(JsonProperty.class); + Object property; + try { + property = field.get(parameter); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + if (property == null) { + if (annotation != null && annotation.required()) { + throw new IllegalArgumentException(field.getName() + " is required and cannot be null."); + } + } else { + try { + Class propertyType = property.getClass(); + if (TypeToken.of(List.class).isAssignableFrom(propertyType)) { + List items = (List) property; + for (Object item : items) { + Validator.validate(item); + } + } + else if (TypeToken.of(Map.class).isAssignableFrom(propertyType)) { + Map entries = (Map) property; + for (Map.Entry entry : entries.entrySet()) { + Validator.validate(entry.getKey()); + Validator.validate(entry.getValue()); + } + } + else if (parameterType != propertyType) { + Validator.validate(property); + } + } catch (IllegalArgumentException ex) { + if (ex.getCause() == null) { + // Build property chain + throw new IllegalArgumentException(field.getName() + "." + ex.getMessage()); + } else { + throw ex; + } + } + } + } + } + } + + /** + * Validates a user provided required parameter to be not null. Returns if + * the parameter passes the validation. An {@link IllegalArgumentException} is passed + * to the {@link ServiceCallback#failure(Throwable)} if a property fails the validation. + * + * @param parameter the parameter to validate + * @param serviceCallback the callback to call with the failure + */ + public static void validate(Object parameter, ServiceCallback serviceCallback) { + try { + validate(parameter); + } catch (IllegalArgumentException ex) { + serviceCallback.failure(ex); + } + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/credentials/BasicAuthenticationCredentials.java b/client-runtime/src/main/java/com/microsoft/rest/credentials/BasicAuthenticationCredentials.java new file mode 100644 index 0000000000000..c63133e12809c --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/credentials/BasicAuthenticationCredentials.java @@ -0,0 +1,60 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.credentials; + +import okhttp3.OkHttpClient; + +/** + * Basic Auth credentials for use with a REST Service Client. + */ +public class BasicAuthenticationCredentials implements ServiceClientCredentials { + + /** + * Basic auth UserName. + */ + private String userName; + + /** + * Basic auth password. + */ + private String password; + + /** + * Instantiates a new basic authentication credential. + * + * @param userName basic auth user name + * @param password basic auth password + */ + public BasicAuthenticationCredentials(String userName, String password) { + this.userName = userName; + this.password = password; + } + + /** + * Get the user name of the credential. + * + * @return the user name + */ + public String getUserName() { + return userName; + } + + /** + * Get the password of the credential. + * + * @return the password + */ + public String getPassword() { + return password; + } + + @Override + public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { + clientBuilder.interceptors().add(new BasicAuthenticationCredentialsInterceptor(this)); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/credentials/BasicAuthenticationCredentialsInterceptor.java b/client-runtime/src/main/java/com/microsoft/rest/credentials/BasicAuthenticationCredentialsInterceptor.java new file mode 100644 index 0000000000000..4d35226e17cf3 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/credentials/BasicAuthenticationCredentialsInterceptor.java @@ -0,0 +1,45 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.credentials; + +import com.google.common.io.BaseEncoding; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +/** + * Basic Auth credentials interceptor for placing a basic auth credential into request headers. + */ +public class BasicAuthenticationCredentialsInterceptor implements Interceptor { + /** + * The credentials instance to apply to the HTTP client pipeline. + */ + private BasicAuthenticationCredentials credentials; + + /** + * Initialize a BasicAuthenticationCredentialsFilter class with a + * BasicAuthenticationCredentials credential. + * + * @param credentials a BasicAuthenticationCredentials instance + */ + public BasicAuthenticationCredentialsInterceptor(BasicAuthenticationCredentials credentials) { + this.credentials = credentials; + } + + @Override + public Response intercept(Chain chain) throws IOException { + String auth = credentials.getUserName() + ":" + credentials.getPassword(); + auth = BaseEncoding.base64().encode(auth.getBytes("UTF8")); + Request newRequest = chain.request().newBuilder() + .header("Authorization", "Basic " + auth) + .build(); + return chain.proceed(newRequest); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/credentials/ServiceClientCredentials.java b/client-runtime/src/main/java/com/microsoft/rest/credentials/ServiceClientCredentials.java new file mode 100644 index 0000000000000..89d15a9d750ac --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/credentials/ServiceClientCredentials.java @@ -0,0 +1,23 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.credentials; + +import okhttp3.OkHttpClient; + +/** + * ServiceClientCredentials is the abstraction for credentials used by + * ServiceClients accessing REST services. + */ +public interface ServiceClientCredentials { + /** + * Apply the credentials to the HTTP client builder. + * + * @param clientBuilder the builder for building up an {@link OkHttpClient} + */ + void applyCredentialsFilter(OkHttpClient.Builder clientBuilder); +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/credentials/TokenCredentials.java b/client-runtime/src/main/java/com/microsoft/rest/credentials/TokenCredentials.java new file mode 100644 index 0000000000000..a82c43e15011e --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/credentials/TokenCredentials.java @@ -0,0 +1,78 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.credentials; + +import okhttp3.OkHttpClient; + +import java.io.IOException; + +/** + * Token based credentials for use with a REST Service Client. + */ +public class TokenCredentials implements ServiceClientCredentials { + /** The authentication scheme. */ + protected String scheme; + + /** The secure token. */ + protected String token; + + /** + * Initializes a new instance of the TokenCredentials. + * + * @param scheme scheme to use. If null, defaults to Bearer + * @param token valid token + */ + public TokenCredentials(String scheme, String token) { + if (scheme == null) { + scheme = "Bearer"; + } + this.scheme = scheme; + this.token = token; + } + + /** + * Get the secure token. + * + * @return the secure token. + * @throws IOException exception thrown from token acquisition operations. + */ + public String getToken() throws IOException { + return token; + } + + /** + * Refresh the secure token. + * @throws IOException exception thrown from token acquisition operations. + */ + public void refreshToken() throws IOException { + // do nothing + } + + /** + * Set the secure token. + * + * @param token the token string + */ + public void setToken(String token) { + this.token = token; + } + + /** + * Get the authentication scheme. + * + * @return the authentication scheme + */ + public String getScheme() { + return scheme; + } + + @Override + public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { + clientBuilder.interceptors().add(new TokenCredentialsInterceptor(this)); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/credentials/TokenCredentialsInterceptor.java b/client-runtime/src/main/java/com/microsoft/rest/credentials/TokenCredentialsInterceptor.java new file mode 100644 index 0000000000000..2054c5a6e258d --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/credentials/TokenCredentialsInterceptor.java @@ -0,0 +1,51 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.credentials; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +/** + * Token credentials filter for placing a token credential into request headers. + */ +public class TokenCredentialsInterceptor implements Interceptor { + /** + * The credentials instance to apply to the HTTP client pipeline. + */ + private TokenCredentials credentials; + + /** + * Initialize a TokenCredentialsFilter class with a + * TokenCredentials credential. + * + * @param credentials a TokenCredentials instance + */ + public TokenCredentialsInterceptor(TokenCredentials credentials) { + this.credentials = credentials; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Response response = sendRequestWithAuthorization(chain); + if (response == null || response.code() == 401) { + credentials.refreshToken(); + response = sendRequestWithAuthorization(chain); + } + return response; + } + + private Response sendRequestWithAuthorization(Chain chain) throws IOException { + Request newRequest = chain.request().newBuilder() + .header("Authorization", credentials.getScheme() + " " + credentials.getToken()) + .build(); + return chain.proceed(newRequest); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/credentials/package-info.java b/client-runtime/src/main/java/com/microsoft/rest/credentials/package-info.java new file mode 100644 index 0000000000000..d5a2aec9cce02 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/credentials/package-info.java @@ -0,0 +1,5 @@ +/** + * The package provides 2 basic credential classes that work with AutoRest + * generated clients for authentication purposes. + */ +package com.microsoft.rest.credentials; \ No newline at end of file diff --git a/client-runtime/src/main/java/com/microsoft/rest/package-info.java b/client-runtime/src/main/java/com/microsoft/rest/package-info.java new file mode 100644 index 0000000000000..9229a121aea2d --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/package-info.java @@ -0,0 +1,6 @@ +/** + * The package contains the runtime classes required for AutoRest generated + * clients to compile and function. To learn more about AutoRest generator, + * see https://github.com/azure/autorest. + */ +package com.microsoft.rest; \ No newline at end of file diff --git a/client-runtime/src/main/java/com/microsoft/rest/retry/ExponentialBackoffRetryStrategy.java b/client-runtime/src/main/java/com/microsoft/rest/retry/ExponentialBackoffRetryStrategy.java new file mode 100644 index 0000000000000..1af917a4eb05d --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/retry/ExponentialBackoffRetryStrategy.java @@ -0,0 +1,106 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.retry; + +import okhttp3.Response; + +/** + * A retry strategy with backoff parameters for calculating the exponential delay between retries. + */ +public class ExponentialBackoffRetryStrategy extends RetryStrategy { + /** + * Represents the default amount of time used when calculating a random delta in the exponential + * delay between retries. + */ + public static final int DEFAULT_CLIENT_BACKOFF = 1000 * 10; + /** + * Represents the default maximum amount of time used when calculating the exponential + * delay between retries. + */ + public static final int DEFAULT_MAX_BACKOFF = 1000 * 30; + /** + *Represents the default minimum amount of time used when calculating the exponential + * delay between retries. + */ + public static final int DEFAULT_MIN_BACKOFF = 1000; + + /** + * The value that will be used to calculate a random delta in the exponential delay + * between retries. + */ + private final int deltaBackoff; + /** + * The maximum backoff time. + */ + private final int maxBackoff; + /** + * The minimum backoff time. + */ + private final int minBackoff; + /** + * The maximum number of retry attempts. + */ + private final int retryCount; + + /** + * Initializes a new instance of the {@link ExponentialBackoffRetryStrategy} class. + */ + public ExponentialBackoffRetryStrategy() { + this(DEFAULT_CLIENT_RETRY_COUNT, DEFAULT_MIN_BACKOFF, DEFAULT_MAX_BACKOFF, DEFAULT_CLIENT_BACKOFF); + } + + /** + * Initializes a new instance of the {@link ExponentialBackoffRetryStrategy} class. + * + * @param retryCount The maximum number of retry attempts. + * @param minBackoff The minimum backoff time. + * @param maxBackoff The maximum backoff time. + * @param deltaBackoff The value that will be used to calculate a random delta in the exponential delay + * between retries. + */ + public ExponentialBackoffRetryStrategy(int retryCount, int minBackoff, int maxBackoff, int deltaBackoff) { + this(null, retryCount, minBackoff, maxBackoff, deltaBackoff, DEFAULT_FIRST_FAST_RETRY); + } + + /** + * Initializes a new instance of the {@link ExponentialBackoffRetryStrategy} class. + * + * @param name The name of the retry strategy. + * @param retryCount The maximum number of retry attempts. + * @param minBackoff The minimum backoff time. + * @param maxBackoff The maximum backoff time. + * @param deltaBackoff The value that will be used to calculate a random delta in the exponential delay + * between retries. + * @param firstFastRetry true to immediately retry in the first attempt; otherwise, false. The subsequent + * retries will remain subject to the configured retry interval. + */ + public ExponentialBackoffRetryStrategy(String name, int retryCount, int minBackoff, int maxBackoff, + int deltaBackoff, boolean firstFastRetry) { + super(name, firstFastRetry); + this.retryCount = retryCount; + this.minBackoff = minBackoff; + this.maxBackoff = maxBackoff; + this.deltaBackoff = deltaBackoff; + } + + /** + * Returns if a request should be retried based on the retry count, current response, + * and the current strategy. + * + * @param retryCount The current retry attempt count. + * @param response The exception that caused the retry conditions to occur. + * @return true if the request should be retried; false otherwise. + */ + @Override + public boolean shouldRetry(int retryCount, Response response) { + int code = response.code(); + //CHECKSTYLE IGNORE MagicNumber FOR NEXT 2 LINES + return retryCount < this.retryCount + && (code == 408 || (code >= 500 && code != 501 && code != 505)); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/retry/RetryHandler.java b/client-runtime/src/main/java/com/microsoft/rest/retry/RetryHandler.java new file mode 100644 index 0000000000000..586673e513bd7 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/retry/RetryHandler.java @@ -0,0 +1,84 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.retry; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +/** + * An instance of this interceptor placed in the request pipeline handles retriable errors. + */ +public class RetryHandler implements Interceptor { + /** + * Represents the default number of retries. + */ + private static final int DEFAULT_NUMBER_OF_ATTEMPTS = 3; + /** + * Represents the default value that will be used to calculate a random + * delta in the exponential delay between retries. + */ + private static final int DEFAULT_BACKOFF_DELTA = 1000 * 10; + /** + * Represents the default maximum backoff time. + */ + private static final int DEFAULT_MAX_BACKOFF = 1000 * 10; + /** + * Represents the default minimum backoff time. + */ + private static final int DEFAULT_MIN_BACKOFF = 1000; + + /** + * The retry strategy to use. + */ + private RetryStrategy retryStrategy; + + /** + * Initialized an instance of {@link RetryHandler} class. + * Sets default retry strategy base on Exponential Backoff. + */ + public RetryHandler() { + this.retryStrategy = new ExponentialBackoffRetryStrategy( + DEFAULT_NUMBER_OF_ATTEMPTS, + DEFAULT_MIN_BACKOFF, + DEFAULT_MAX_BACKOFF, + DEFAULT_BACKOFF_DELTA); + } + + /** + * Initialized an instance of {@link RetryHandler} class. + * + * @param retryStrategy retry strategy to use. + */ + public RetryHandler(RetryStrategy retryStrategy) { + this.retryStrategy = retryStrategy; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + + // try the request + Response response = chain.proceed(request); + + int tryCount = 0; + while (retryStrategy.shouldRetry(tryCount, response)) { + tryCount++; + if (response.body() != null) { + response.body().close(); + } + // retry the request + response = chain.proceed(request); + } + + // otherwise just pass the original response on + return response; + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/retry/RetryStrategy.java b/client-runtime/src/main/java/com/microsoft/rest/retry/RetryStrategy.java new file mode 100644 index 0000000000000..d5002a5a2e6bb --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/retry/RetryStrategy.java @@ -0,0 +1,82 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.retry; + +import okhttp3.Response; + +/** + * Represents a retry strategy that determines the number of retry attempts and the interval + * between retries. + */ +public abstract class RetryStrategy { + /** + * Represents the default number of retry attempts. + */ + public static final int DEFAULT_CLIENT_RETRY_COUNT = 10; + + /** + * Represents the default interval between retries. + */ + public static final int DEFAULT_RETRY_INTERVAL = 1000; + /** + * Represents the default flag indicating whether the first retry attempt will be made immediately, + * whereas subsequent retries will remain subject to the retry interval. + */ + public static final boolean DEFAULT_FIRST_FAST_RETRY = true; + + /** + * The name of the retry strategy. + */ + private String name; + + /** + * The value indicating whether the first retry attempt will be made immediately, + * whereas subsequent retries will remain subject to the retry interval. + */ + private boolean fastFirstRetry; + + /** + * Initializes a new instance of the {@link RetryStrategy} class. + * + * @param name The name of the retry strategy. + * @param firstFastRetry true to immediately retry in the first attempt; otherwise, false. + */ + protected RetryStrategy(String name, boolean firstFastRetry) { + this.name = name; + this.fastFirstRetry = firstFastRetry; + } + + /** + * Returns if a request should be retried based on the retry count, current response, + * and the current strategy. + * + * @param retryCount The current retry attempt count. + * @param response The exception that caused the retry conditions to occur. + * @return true if the request should be retried; false otherwise. + */ + public abstract boolean shouldRetry(int retryCount, Response response); + + /** + * Gets the name of the retry strategy. + * + * @return the name of the retry strategy. + */ + public String getName() { + return name; + } + + /** + * Gets whether the first retry attempt will be made immediately. + * + * @return true if the first retry attempt will be made immediately. + * false otherwise. + */ + public boolean isFastFirstRetry() { + return fastFirstRetry; + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/retry/package-info.java b/client-runtime/src/main/java/com/microsoft/rest/retry/package-info.java new file mode 100644 index 0000000000000..2abd7034fa800 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/retry/package-info.java @@ -0,0 +1,5 @@ +/** + * The package contains classes that define the retry behaviors when an error + * occurs during a REST call. + */ +package com.microsoft.rest.retry; \ No newline at end of file diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/ByteArraySerializer.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/ByteArraySerializer.java new file mode 100644 index 0000000000000..e32093a1c1803 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/ByteArraySerializer.java @@ -0,0 +1,41 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import java.io.IOException; + +/** + * Custom serializer for serializing {@link Byte[]} objects into Base64 strings. + */ +public class ByteArraySerializer extends JsonSerializer { + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + SimpleModule module = new SimpleModule(); + module.addSerializer(Byte[].class, new ByteArraySerializer()); + return module; + } + + @Override + public void serialize(Byte[] value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + byte[] bytes = new byte[value.length]; + for (int i = 0; i < value.length; i++) { + bytes[i] = value[i]; + } + jgen.writeBinary(bytes); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/CollectionFormat.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/CollectionFormat.java new file mode 100644 index 0000000000000..12e4c3fdeb01e --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/CollectionFormat.java @@ -0,0 +1,63 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.serializer; + +/** + * Swagger collection format to use for joining {@link java.util.List} parameters in + * paths, queries, and headers. + * See https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#fixed-fields-7. + */ +public enum CollectionFormat { + /** + * Comma separated values. + * E.g. foo,bar + */ + CSV(","), + /** + * Space separated values. + * E.g. foo bar + */ + SSV(" "), + /** + * Tab separated values. + * E.g. foo\tbar + */ + TSV("\t"), + /** + * Pipe(|) separated values. + * E.g. foo|bar + */ + PIPES("|"), + /** + * Corresponds to multiple parameter instances instead of multiple values + * for a single instance. + * E.g. foo=bar&foo=baz + */ + MULTI("&"); + + /** + * The delimiter separating the values. + */ + private String delimiter; + + /** + * Creates an instance of the enum. + * @param delimiter the delimiter as a string. + */ + CollectionFormat(String delimiter) { + this.delimiter = delimiter; + } + + /** + * Gets the delimiter used to join a list of parameters. + * @return the delimiter of the current collection format. + */ + public String getDelimiter() { + return delimiter; + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/DateTimeRfc1123Serializer.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/DateTimeRfc1123Serializer.java new file mode 100644 index 0000000000000..ac90cd471c7ce --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/DateTimeRfc1123Serializer.java @@ -0,0 +1,43 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.microsoft.rest.DateTimeRfc1123; + +import java.io.IOException; + +/** + * Custom serializer for serializing {@link DateTimeRfc1123} object into RFC1123 formats. + */ +public class DateTimeRfc1123Serializer extends JsonSerializer { + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + SimpleModule module = new SimpleModule(); + module.addSerializer(DateTimeRfc1123.class, new DateTimeRfc1123Serializer()); + return module; + } + + @Override + public void serialize(DateTimeRfc1123 value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (provider.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)) { + jgen.writeNumber(value.getDateTime().getMillis()); + } else { + jgen.writeString(value.toString()); //Use the default toString as it is RFC1123. + } + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/DateTimeSerializer.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/DateTimeSerializer.java new file mode 100644 index 0000000000000..30dc795a415b8 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/DateTimeSerializer.java @@ -0,0 +1,46 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.ISODateTimeFormat; + +import java.io.IOException; + +/** + * Custom serializer for serializing {@link DateTime} object into ISO8601 formats. + */ +public class DateTimeSerializer extends JsonSerializer { + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + SimpleModule module = new SimpleModule(); + module.addSerializer(DateTime.class, new DateTimeSerializer()); + return module; + } + + @Override + public void serialize(DateTime value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (provider.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)) { + jgen.writeNumber(value.getMillis()); + } else { + value = value.withZone(DateTimeZone.UTC); + jgen.writeString(value.toString(ISODateTimeFormat.dateTime())); + } + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/FlatteningDeserializer.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/FlatteningDeserializer.java new file mode 100644 index 0000000000000..dd7468a728372 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/FlatteningDeserializer.java @@ -0,0 +1,110 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.serializer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.IOException; +import java.lang.reflect.Field; + +/** + * Custom serializer for deserializing complex types with wrapped properties. + * For example, a property with annotation @JsonProperty(value = "properties.name") + * will be mapped to a top level "name" property in the POJO model. + */ +public class FlatteningDeserializer extends StdDeserializer implements ResolvableDeserializer { + /** + * The default mapperAdapter for the current type. + */ + private final JsonDeserializer defaultDeserializer; + + /** + * The object mapper for default deserializations. + */ + private final ObjectMapper mapper; + + /** + * Creates an instance of FlatteningDeserializer. + * @param vc handled type + * @param defaultDeserializer the default JSON mapperAdapter + * @param mapper the object mapper for default deserializations + */ + protected FlatteningDeserializer(Class vc, JsonDeserializer defaultDeserializer, ObjectMapper mapper) { + super(vc); + this.defaultDeserializer = defaultDeserializer; + this.mapper = mapper; + } + + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @param mapper the object mapper for default deserializations + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule(final ObjectMapper mapper) { + SimpleModule module = new SimpleModule(); + module.setDeserializerModifier(new BeanDeserializerModifier() { + @Override + public JsonDeserializer modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer deserializer) { + if (beanDesc.getBeanClass().getAnnotation(JsonFlatten.class) != null) { + return new FlatteningDeserializer(beanDesc.getBeanClass(), deserializer, mapper); + } + return deserializer; + } + }); + return module; + } + + @SuppressWarnings("unchecked") + @Override + public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + JsonNode root = mapper.readTree(jp); + final Class tClass = this.defaultDeserializer.handledType(); + for (Field field : tClass.getDeclaredFields()) { + JsonNode node = root; + JsonProperty property = field.getAnnotation(JsonProperty.class); + if (property != null) { + String value = property.value(); + if (value.matches(".+[^\\\\]\\..+")) { + String[] values = value.split("((? implements ResolvableSerializer { + /** + * The default mapperAdapter for the current type. + */ + private final JsonSerializer defaultSerializer; + + /** + * The object mapper for default serializations. + */ + private final ObjectMapper mapper; + + /** + * Creates an instance of FlatteningSerializer. + * @param vc handled type + * @param defaultSerializer the default JSON serializer + * @param mapper the object mapper for default serializations + */ + protected FlatteningSerializer(Class vc, JsonSerializer defaultSerializer, ObjectMapper mapper) { + super(vc, false); + this.defaultSerializer = defaultSerializer; + this.mapper = mapper; + } + + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @param mapper the object mapper for default serializations + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule(final ObjectMapper mapper) { + SimpleModule module = new SimpleModule(); + module.setSerializerModifier(new BeanSerializerModifier() { + @Override + public JsonSerializer modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer serializer) { + if (beanDesc.getBeanClass().getAnnotation(JsonFlatten.class) != null) { + return new FlatteningSerializer(beanDesc.getBeanClass(), serializer, mapper); + } + return serializer; + } + }); + return module; + } + + @Override + public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + return; + } + + // BFS for all collapsed properties + ObjectNode root = mapper.valueToTree(value); + ObjectNode res = root.deepCopy(); + Queue source = new LinkedBlockingQueue(); + Queue target = new LinkedBlockingQueue(); + source.add(root); + target.add(res); + while (!source.isEmpty()) { + ObjectNode current = source.poll(); + ObjectNode resCurrent = target.poll(); + Iterator> fields = current.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + ObjectNode node = resCurrent; + String key = field.getKey(); + JsonNode outNode = resCurrent.get(key); + if (field.getKey().matches(".+[^\\\\]\\..+")) { + String[] values = field.getKey().split("((? 0 + && (field.getValue()).get(0) instanceof ObjectNode) { + Iterator sourceIt = field.getValue().elements(); + Iterator targetIt = outNode.elements(); + while (sourceIt.hasNext()) { + source.add((ObjectNode) sourceIt.next()); + target.add((ObjectNode) targetIt.next()); + } + } + } + } + jgen.writeTree(res); + } + + @Override + public void resolve(SerializerProvider provider) throws JsonMappingException { + ((ResolvableSerializer) defaultSerializer).resolve(provider); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/HeadersSerializer.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/HeadersSerializer.java new file mode 100644 index 0000000000000..a0b109a0fe153 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/HeadersSerializer.java @@ -0,0 +1,49 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import okhttp3.Headers; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Custom serializer for serializing {@link Headers} objects. + */ +public class HeadersSerializer extends JsonSerializer { + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + SimpleModule module = new SimpleModule(); + module.addSerializer(Headers.class, new HeadersSerializer()); + return module; + } + + @Override + public void serialize(Headers value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + Map headers = new HashMap(); + for (Map.Entry> entry : value.toMultimap().entrySet()) { + if (entry.getValue() != null && entry.getValue().size() == 1) { + headers.put(entry.getKey(), entry.getValue().get(0)); + } else if (entry.getValue() != null && entry.getValue().size() > 1) { + headers.put(entry.getKey(), entry.getValue()); + } + } + jgen.writeObject(headers); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonConverterFactory.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonConverterFactory.java new file mode 100644 index 0000000000000..96749a19c9f16 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonConverterFactory.java @@ -0,0 +1,122 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.serializer; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Converter; +import retrofit2.Retrofit; + +import java.io.IOException; +import java.io.Reader; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +/** + * A similar implementation of {@link retrofit2.converter.jackson.JacksonConverterFactory} which supports polymorphism. + */ +public final class JacksonConverterFactory extends Converter.Factory { + /** + * Create an instance using a default {@link ObjectMapper} instance for conversion. + * + * @return an instance of JacksonConverterFactory + */ + public static JacksonConverterFactory create() { + return create(new ObjectMapper()); + } + + + /** + * Create an instance using {@code mapper} for conversion. + * + * @param mapper a user-provided {@link ObjectMapper} to use + * @return an instance of JacksonConverterFactory + */ + public static JacksonConverterFactory create(ObjectMapper mapper) { + return new JacksonConverterFactory(mapper); + } + + /** + * The Jackson object mapper. + */ + private final ObjectMapper mapper; + + private JacksonConverterFactory(ObjectMapper mapper) { + if (mapper == null) { + throw new NullPointerException("mapper == null"); + } + this.mapper = mapper; + } + + @Override + public Converter responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { + JavaType javaType = mapper.getTypeFactory().constructType(type); + ObjectReader reader = mapper.reader(javaType); + return new JacksonResponseBodyConverter<>(reader); + } + + @Override + public Converter requestBodyConverter(Type type, + Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { + ObjectWriter writer = mapper.writer(); + return new JacksonRequestBodyConverter<>(writer); + } + + /** + * An instance of this class converts an object into JSON. + * + * @param type of request object + */ + final class JacksonRequestBodyConverter implements Converter { + /** Jackson object writer. */ + private final ObjectWriter adapter; + + JacksonRequestBodyConverter(ObjectWriter adapter) { + this.adapter = adapter; + } + + @Override public RequestBody convert(T value) throws IOException { + byte[] bytes = adapter.writeValueAsBytes(value); + return RequestBody.create(MediaType.parse("application/json; charset=UTF-8"), bytes); + } + } + + /** + * An instance of this class converts a JSON payload into an object. + * + * @param the expected object type to convert to + */ + final class JacksonResponseBodyConverter implements Converter { + /** Jackson object reader. */ + private final ObjectReader adapter; + + JacksonResponseBodyConverter(ObjectReader adapter) { + this.adapter = adapter; + } + + @Override public T convert(ResponseBody value) throws IOException { + Reader reader = value.charStream(); + try { + return adapter.readValue(reader); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignored) { + } + } + } + } + } +} + diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonMapperAdapter.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonMapperAdapter.java new file mode 100644 index 0000000000000..a59247207502b --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonMapperAdapter.java @@ -0,0 +1,185 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.serializer; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.joda.JodaModule; +import com.google.common.base.CharMatcher; +import com.google.common.base.Joiner; + +import java.io.IOException; +import java.io.StringWriter; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * A serialization helper class wrapped around {@link JacksonConverterFactory} and {@link ObjectMapper}. + */ +public class JacksonMapperAdapter { + /** + * An instance of {@link ObjectMapper} to serialize/deserialize objects. + */ + private ObjectMapper mapper; + + /** + * An instance of {@link ObjectMapper} that does not do flattening. + */ + private ObjectMapper simpleMapper; + + /** + * An instance of {@link JacksonConverterFactory} for Retrofit to use. + */ + private JacksonConverterFactory converterFactory; + + /** + * Initializes an instance of JacksonMapperAdapter with default configurations + * applied to the object mapper. + * + * @param mapper the object mapper to use. + */ + protected void initializeObjectMapper(ObjectMapper mapper) { + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(new JodaModule()) + .registerModule(ByteArraySerializer.getModule()) + .registerModule(DateTimeSerializer.getModule()) + .registerModule(DateTimeRfc1123Serializer.getModule()) + .registerModule(HeadersSerializer.getModule()); + mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker() + .withFieldVisibility(JsonAutoDetect.Visibility.ANY) + .withSetterVisibility(JsonAutoDetect.Visibility.NONE) + .withGetterVisibility(JsonAutoDetect.Visibility.NONE) + .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)); + } + + /** + * Gets a static instance of {@link ObjectMapper} that doesn't handle flattening. + * + * @return an instance of {@link ObjectMapper}. + */ + protected ObjectMapper getSimpleMapper() { + if (simpleMapper == null) { + simpleMapper = new ObjectMapper(); + initializeObjectMapper(simpleMapper); + } + return simpleMapper; + } + + /** + * Gets a static instance of {@link ObjectMapper}. + * + * @return an instance of {@link ObjectMapper}. + */ + public ObjectMapper getObjectMapper() { + if (mapper == null) { + mapper = new ObjectMapper(); + initializeObjectMapper(mapper); + mapper.registerModule(FlatteningSerializer.getModule(getSimpleMapper())) + .registerModule(FlatteningDeserializer.getModule(getSimpleMapper())); + } + return mapper; + } + + /** + * Gets a static instance of JacksonConverter factory. + * + * @return an instance of JacksonConverter factory. + */ + public JacksonConverterFactory getConverterFactory() { + if (converterFactory == null) { + converterFactory = JacksonConverterFactory.create(getObjectMapper()); + } + return converterFactory; + } + + /** + * Serializes an object into a JSON string using the current {@link ObjectMapper}. + * + * @param object the object to serialize. + * @return the serialized string. Null if the object to serialize is null. + * @throws IOException exception from serialization. + */ + public String serialize(Object object) throws IOException { + if (object == null) { + return null; + } + StringWriter writer = new StringWriter(); + getObjectMapper().writeValue(writer, object); + return writer.toString(); + } + + /** + * Serializes an object into a raw string using the current {@link ObjectMapper}. + * The leading and trailing quotes will be trimmed. + * + * @param object the object to serialize. + * @return the serialized string. Null if the object to serialize is null. + */ + public String serializeRaw(Object object) { + if (object == null) { + return null; + } + try { + return CharMatcher.is('"').trimFrom(serialize(object)); + } catch (IOException ex) { + return null; + } + } + + /** + * Serializes a list into a string with the delimiter specified with the + * Swagger collection format joining each individual serialized items in + * the list. + * + * @param list the list to serialize. + * @param format the Swagger collection format. + * @return the serialized string + */ + public String serializeList(List list, CollectionFormat format) { + if (list == null) { + return null; + } + List serialized = new ArrayList<>(); + for (Object element : list) { + String raw = serializeRaw(element); + serialized.add(raw != null ? raw : ""); + } + return Joiner.on(format.getDelimiter()).join(serialized); + } + + /** + * Deserializes a string into a {@link T} object using the current {@link ObjectMapper}. + * + * @param value the string value to deserialize. + * @param the type of the deserialized object. + * @param type the type to deserialize. + * @return the deserialized object. + * @throws IOException exception in deserialization + */ + @SuppressWarnings("unchecked") + public T deserialize(String value, final Type type) throws IOException { + if (value == null || value.isEmpty()) { + return null; + } + return (T) getObjectMapper().readValue(value, new TypeReference() { + @Override + public Type getType() { + return type; + } + }); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/JsonFlatten.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/JsonFlatten.java new file mode 100644 index 0000000000000..070447cad973c --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/JsonFlatten.java @@ -0,0 +1,25 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.serializer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used for flattening properties separated by '.'. + * E.g. a property with JsonProperty value "properties.value" + * will have "value" property under the "properties" tree on + * the wire. + * + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonFlatten { +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/package-info.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/package-info.java new file mode 100644 index 0000000000000..7c8d20b9d256a --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/package-info.java @@ -0,0 +1,5 @@ +/** + * The package contains classes that handle serialization and deserialization + * for the REST call payloads. + */ +package com.microsoft.rest.serializer; \ No newline at end of file diff --git a/client-runtime/src/test/java/com/microsoft/rest/CredentialsTests.java b/client-runtime/src/test/java/com/microsoft/rest/CredentialsTests.java new file mode 100644 index 0000000000000..d21de3ef9bd1e --- /dev/null +++ b/client-runtime/src/test/java/com/microsoft/rest/CredentialsTests.java @@ -0,0 +1,71 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import com.microsoft.rest.credentials.BasicAuthenticationCredentials; +import com.microsoft.rest.credentials.TokenCredentials; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import retrofit2.Retrofit; + +public class CredentialsTests { + @Test + public void basicCredentialsTest() throws Exception { + BasicAuthenticationCredentials credentials = new BasicAuthenticationCredentials("user", "pass"); + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + credentials.applyCredentialsFilter(clientBuilder); + clientBuilder.addInterceptor( + new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + String header = chain.request().header("Authorization"); + Assert.assertEquals("Basic dXNlcjpwYXNz", header); + return new Response.Builder() + .request(chain.request()) + .code(200) + .protocol(Protocol.HTTP_1_1) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, new Retrofit.Builder()) { }; + Response response = serviceClient.httpClient().newCall(new Request.Builder().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } + + @Test + public void tokenCredentialsTest() throws Exception { + TokenCredentials credentials = new TokenCredentials(null, "this_is_a_token"); + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + credentials.applyCredentialsFilter(clientBuilder); + clientBuilder.addInterceptor( + new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + String header = chain.request().header("Authorization"); + Assert.assertEquals("Bearer this_is_a_token", header); + return new Response.Builder() + .request(chain.request()) + .code(200) + .protocol(Protocol.HTTP_1_1) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, new Retrofit.Builder()) { }; + Response response = serviceClient.httpClient().newCall(new Request.Builder().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } +} diff --git a/client-runtime/src/test/java/com/microsoft/rest/RetryHandlerTests.java b/client-runtime/src/test/java/com/microsoft/rest/RetryHandlerTests.java new file mode 100644 index 0000000000000..068e57b214323 --- /dev/null +++ b/client-runtime/src/test/java/com/microsoft/rest/RetryHandlerTests.java @@ -0,0 +1,74 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import com.microsoft.rest.retry.RetryHandler; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import retrofit2.Retrofit; + +public class RetryHandlerTests { + @Test + public void exponentialRetryEndOn501() throws Exception { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + Retrofit.Builder retrofitBuilder = new Retrofit.Builder(); + clientBuilder.addInterceptor(new RetryHandler()); + clientBuilder.addInterceptor(new Interceptor() { + // Send 408, 500, 502, all retried, with a 501 ending + private int[] codes = new int[]{408, 500, 502, 501}; + private int count = 0; + + @Override + public Response intercept(Chain chain) throws IOException { + return new Response.Builder() + .request(chain.request()) + .code(codes[count++]) + .protocol(Protocol.HTTP_1_1) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, retrofitBuilder) { }; + Response response = serviceClient.httpClient().newCall( + new Request.Builder().url("http://localhost").get().build()).execute(); + Assert.assertEquals(501, response.code()); + } + + @Test + public void exponentialRetryMax() throws Exception { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + Retrofit.Builder retrofitBuilder = new Retrofit.Builder(); + clientBuilder.addInterceptor(new RetryHandler()); + clientBuilder.addInterceptor(new Interceptor() { + // Send 500 until max retry is hit + private int count = 0; + + @Override + public Response intercept(Chain chain) throws IOException { + Assert.assertTrue(count++ < 5); + return new Response.Builder() + .request(chain.request()) + .code(500) + .protocol(Protocol.HTTP_1_1) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, retrofitBuilder) { }; + Response response = serviceClient.httpClient().newCall( + new Request.Builder().url("http://localhost").get().build()).execute(); + Assert.assertEquals(500, response.code()); + } +} diff --git a/client-runtime/src/test/java/com/microsoft/rest/ServiceClientTests.java b/client-runtime/src/test/java/com/microsoft/rest/ServiceClientTests.java new file mode 100644 index 0000000000000..9d76292d20a03 --- /dev/null +++ b/client-runtime/src/test/java/com/microsoft/rest/ServiceClientTests.java @@ -0,0 +1,59 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import retrofit2.Retrofit; + +public class ServiceClientTests { + @Test + public void filterTests() throws Exception { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + Retrofit.Builder retrofitBuilder = new Retrofit.Builder(); + clientBuilder.interceptors().add(0, new FirstFilter()); + clientBuilder.interceptors().add(1, new SecondFilter()); + clientBuilder.interceptors().add(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + Assert.assertEquals("1", chain.request().header("filter1")); + Assert.assertEquals("2", chain.request().header("filter2")); + return new Response.Builder() + .request(chain.request()) + .code(200) + .protocol(Protocol.HTTP_1_1) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, retrofitBuilder) { }; + Response response = serviceClient.httpClient().newCall(new Request.Builder().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } + + public class FirstFilter implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(chain.request().newBuilder().header("filter1", "1").build()); + } + } + + public class SecondFilter implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(chain.request().newBuilder().header("filter2", "2").build()); + } + } +} diff --git a/client-runtime/src/test/java/com/microsoft/rest/UserAgentTests.java b/client-runtime/src/test/java/com/microsoft/rest/UserAgentTests.java new file mode 100644 index 0000000000000..9aa60e5b26313 --- /dev/null +++ b/client-runtime/src/test/java/com/microsoft/rest/UserAgentTests.java @@ -0,0 +1,66 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import retrofit2.Retrofit; + +public class UserAgentTests { + @Test + public void defaultUserAgentTests() throws Exception { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() + .addInterceptor(new UserAgentInterceptor()) + .addInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + String header = chain.request().header("User-Agent"); + Assert.assertEquals("AutoRest-Java", header); + return new Response.Builder() + .request(chain.request()) + .code(200) + .protocol(Protocol.HTTP_1_1) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, new Retrofit.Builder()) { }; + Response response = serviceClient.httpClient() + .newCall(new Request.Builder().get().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } + + @Test + public void customUserAgentTests() throws Exception { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() + .addInterceptor(new UserAgentInterceptor().withUserAgent("Awesome")) + .addInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + String header = chain.request().header("User-Agent"); + Assert.assertEquals("Awesome", header); + return new Response.Builder() + .request(chain.request()) + .code(200) + .protocol(Protocol.HTTP_1_1) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, new Retrofit.Builder()) { }; + Response response = serviceClient.httpClient() + .newCall(new Request.Builder().get().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } +} diff --git a/client-runtime/src/test/java/com/microsoft/rest/ValidatorTests.java b/client-runtime/src/test/java/com/microsoft/rest/ValidatorTests.java new file mode 100644 index 0000000000000..5120c33df0cef --- /dev/null +++ b/client-runtime/src/test/java/com/microsoft/rest/ValidatorTests.java @@ -0,0 +1,195 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.TextNode; + +import org.junit.Assert; +import org.joda.time.LocalDate; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.fail; + +public class ValidatorTests { + @Test + public void validateInt() throws Exception { + IntWrapper body = new IntWrapper(); + body.value = 2; + body.nullable = null; + Validator.validate(body); // pass + } + + @Test + public void validateInteger() throws Exception { + IntegerWrapper body = new IntegerWrapper(); + body.value = 3; + Validator.validate(body); // pass + try { + body.value = null; + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("value is required")); + } + } + + @Test + public void validateString() throws Exception { + StringWrapper body = new StringWrapper(); + body.value = ""; + Validator.validate(body); // pass + try { + body.value = null; + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("value is required")); + } + } + + @Test + public void validateLocalDate() throws Exception { + LocalDateWrapper body = new LocalDateWrapper(); + body.value = new LocalDate(1, 2, 3); + Validator.validate(body); // pass + try { + body.value = null; + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("value is required")); + } + } + + @Test + public void validateList() throws Exception { + ListWrapper body = new ListWrapper(); + try { + body.list = null; + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("list is required")); + } + body.list = new ArrayList(); + Validator.validate(body); // pass + StringWrapper wrapper = new StringWrapper(); + wrapper.value = "valid"; + body.list.add(wrapper); + Validator.validate(body); // pass + body.list.add(null); + Validator.validate(body); // pass + body.list.add(new StringWrapper()); + try { + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("list.value is required")); + } + } + + @Test + public void validateMap() throws Exception { + MapWrapper body = new MapWrapper(); + try { + body.map = null; + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("map is required")); + } + body.map = new HashMap(); + Validator.validate(body); // pass + StringWrapper wrapper = new StringWrapper(); + wrapper.value = "valid"; + body.map.put(new LocalDate(1, 2, 3), wrapper); + Validator.validate(body); // pass + body.map.put(new LocalDate(1, 2, 3), null); + Validator.validate(body); // pass + body.map.put(new LocalDate(1, 2, 3), new StringWrapper()); + try { + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("map.value is required")); + } + } + + @Test + public void validateObject() throws Exception { + Product product = new Product(); + Validator.validate(product); + } + + @Test + public void validateRecursive() throws Exception { + TextNode textNode = new TextNode("\"\""); + Validator.validate(textNode); + } + + public final class IntWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 2 LINES + public int value; + public Object nullable; + } + + public final class IntegerWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public Integer value; + } + + public final class StringWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public String value; + } + + public final class LocalDateWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public LocalDate value; + } + + public final class ListWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public List list; + } + + public final class MapWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public Map map; + } + + public enum Color { + RED, + GREEN, + Blue + } + + public final class EnumWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public Color color; + } + + public final class Product { + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 2 LINES + public String id; + public String tag; + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000000..aa77260c5893f --- /dev/null +++ b/pom.xml @@ -0,0 +1,220 @@ + + + 4.0.0 + + com.microsoft.azure + autorest-clientruntime-for-java + 1.0.0-SNAPSHOT + pom + + AutoRest Client Runtimes for Java + This package contains the runtimes for AutoRest generated Java clients. + https://github.com/Azure/autorest-clientruntime-for-java + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + + scm:git:https://github.com/Azure/autorest-clientruntime-for-java + scm:git:git@github.com:Azure/autorest-clientruntime-for-java.git + HEAD + + + + UTF-8 + + + + + + microsoft + Microsoft + + + + + + ossrh + Sonatype Snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + default + + true + always + + + + + + + ossrh + Maven Central Snapshots + https://oss.sonatype.org/content/repositories/snapshots + true + default + + + ossrh + Maven Central + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + com.google.guava + guava + 18.0 + + + com.squareup.retrofit2 + retrofit + 2.0.2 + + + com.squareup.okhttp3 + okhttp + 3.3.1 + + + com.squareup.okhttp3 + logging-interceptor + 3.3.1 + + + com.squareup.okhttp3 + okhttp-urlconnection + 3.3.1 + + + com.squareup.retrofit2 + converter-jackson + 2.0.2 + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + 2.7.2 + + + org.apache.commons + commons-lang3 + 3.4 + + + com.microsoft.azure + adal4j + 1.1.2 + + + com.microsoft.aad + adal + 1.1.11 + + + junit + junit + 4.12 + test + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 2.17 + + + com.microsoft.azure + autorest-build-tools + 1.0.0-SNAPSHOT + + + com.puppycrawl.tools + checkstyle + 6.18 + + + + checkstyle.xml + samedir=build-tools/src/main/resources + suppressions.xml + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + true + true + -Xlint:unchecked + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.8 + + *.implementation.*;*.utils.*;com.microsoft.schemas._2003._10.serialization;*.blob.core.storage + /** +
* Copyright (c) Microsoft Corporation. All rights reserved. +
* Licensed under the MIT License. See License.txt in the project root for +
* license information. +
*/]]>
+
+
+
+ + + + org.apache.maven.plugins + maven-resources-plugin + 2.4.3 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18.1 + + + **/Test*.java + **/*Test.java + **/*Tests.java + **/*TestCase.java + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.2 + + + +
+ + build-tools + client-runtime + azure-client-runtime + azure-client-authentication + +
From 138bca9f38c4d8b5c3508f07de48b1c0344e1415 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Mon, 11 Jul 2016 16:00:06 -0700 Subject: [PATCH 25/27] Support Base64url (#1238) --- .../java/com/microsoft/rest/Base64Url.java | 94 +++++++++++++++++++ .../rest/serializer/Base64UrlSerializer.java | 38 ++++++++ .../rest/serializer/JacksonMapperAdapter.java | 1 + 3 files changed, 133 insertions(+) create mode 100644 client-runtime/src/main/java/com/microsoft/rest/Base64Url.java create mode 100644 client-runtime/src/main/java/com/microsoft/rest/serializer/Base64UrlSerializer.java diff --git a/client-runtime/src/main/java/com/microsoft/rest/Base64Url.java b/client-runtime/src/main/java/com/microsoft/rest/Base64Url.java new file mode 100644 index 0000000000000..61b02879feae1 --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/Base64Url.java @@ -0,0 +1,94 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest; + +import com.google.common.io.BaseEncoding; + +import java.util.Arrays; + +/** + * Simple wrapper over Base64Url encoded byte array used during serialization/deserialization. + */ +public final class Base64Url { + /** + * The Base64Url encoded bytes. + */ + private final byte[] bytes; + + /** + * Creates a new Base64Url object with the specified encoded string. + * + * @param string The encoded string. + */ + private Base64Url(String string) { + if (string == null) { + this.bytes = null; + } else { + this.bytes = string.getBytes(); + } + } + + /** + * Encode a byte array into Base64Url encoded bytes. + * + * @param bytes The byte array to encode. + * @return a Base64Url instance + */ + public static Base64Url encode(byte[] bytes) { + if (bytes == null) { + return new Base64Url(null); + } else { + return new Base64Url(BaseEncoding.base64Url().omitPadding().encode(bytes)); + } + } + + /** + * Returns the underlying encoded byte array. + * + * @return The underlying encoded byte array. + */ + public byte[] getEncodedBytes() { + return bytes; + } + + /** + * Decode the bytes and return. + * + * @return The decoded byte array. + */ + public byte[] getDecodedBytes() { + if (this.bytes == null) { + return null; + } + return BaseEncoding.base64Url().decode(new String(bytes)); + } + + @Override + public String toString() { + return new String(bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (!(obj instanceof Base64Url)) { + return false; + } + + Base64Url rhs = (Base64Url) obj; + return Arrays.equals(this.bytes, rhs.getEncodedBytes()); + } +} \ No newline at end of file diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/Base64UrlSerializer.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/Base64UrlSerializer.java new file mode 100644 index 0000000000000..757ab735e2c0d --- /dev/null +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/Base64UrlSerializer.java @@ -0,0 +1,38 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.rest.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.microsoft.rest.Base64Url; + +import java.io.IOException; + +/** + * Custom serializer for serializing {@link Byte[]} objects into Base64 strings. + */ +public class Base64UrlSerializer extends JsonSerializer { + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + SimpleModule module = new SimpleModule(); + module.addSerializer(Base64Url.class, new Base64UrlSerializer()); + return module; + } + + @Override + public void serialize(Base64Url value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeString(value.toString()); + } +} diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonMapperAdapter.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonMapperAdapter.java index a59247207502b..ea938fe50c9c5 100644 --- a/client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonMapperAdapter.java +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/JacksonMapperAdapter.java @@ -56,6 +56,7 @@ protected void initializeObjectMapper(ObjectMapper mapper) { .setSerializationInclusion(JsonInclude.Include.NON_NULL) .registerModule(new JodaModule()) .registerModule(ByteArraySerializer.getModule()) + .registerModule(Base64UrlSerializer.getModule()) .registerModule(DateTimeSerializer.getModule()) .registerModule(DateTimeRfc1123Serializer.getModule()) .registerModule(HeadersSerializer.getModule()); From f0805a77ca435f5dba08813742d439364a278dee Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Tue, 12 Jul 2016 10:04:34 -0700 Subject: [PATCH 26/27] Use beta2 for other packages --- azure-android-client-authentication/build.gradle | 4 ++-- azure-client-authentication/build.gradle | 4 ++-- azure-client-authentication/pom.xml | 4 ++-- azure-client-runtime/build.gradle | 4 ++-- azure-client-runtime/pom.xml | 2 +- build-tools/pom.xml | 2 +- client-runtime/build.gradle | 2 +- client-runtime/pom.xml | 2 +- pom.xml | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/azure-android-client-authentication/build.gradle b/azure-android-client-authentication/build.gradle index 2651d8389383e..c557f363b0093 100644 --- a/azure-android-client-authentication/build.gradle +++ b/azure-android-client-authentication/build.gradle @@ -18,7 +18,7 @@ android { minSdkVersion 15 targetSdkVersion 23 versionCode 1 - versionName "1.0.0-SNAPSHOT" + versionName "1.0.0-beta2" } buildTypes { @@ -51,7 +51,7 @@ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.0.1' compile 'com.microsoft.aad:adal:1.1.11' - compile 'com.microsoft.azure:azure-client-runtime:1.0.0-SNAPSHOT' + compile 'com.microsoft.azure:azure-client-runtime:1.0.0-beta2' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-authentication/build.gradle b/azure-client-authentication/build.gradle index 2b40ada004e0e..f22be08ed8d0e 100644 --- a/azure-client-authentication/build.gradle +++ b/azure-client-authentication/build.gradle @@ -10,7 +10,7 @@ buildscript { apply plugin: 'java' apply plugin: 'checkstyle' -version = '1.0.0-SNAPSHOT' +version = '1.0.0-beta2' checkstyle { toolVersion = "6.18" @@ -21,7 +21,7 @@ checkstyle { dependencies { compile 'com.microsoft.azure:adal4j:1.1.2' - compile 'com.microsoft.azure:azure-client-runtime:1.0.0-SNAPSHOT' + compile 'com.microsoft.azure:azure-client-runtime:1.0.0-beta2' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-authentication/pom.xml b/azure-client-authentication/pom.xml index 47582ca1eaa87..ecc305c3ecedd 100644 --- a/azure-client-authentication/pom.xml +++ b/azure-client-authentication/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-SNAPSHOT + 1.0.0-beta2 ../pom.xml @@ -49,7 +49,7 @@ com.microsoft.azure azure-client-runtime - 1.0.0-SNAPSHOT + 1.0.0-beta2 com.microsoft.azure diff --git a/azure-client-runtime/build.gradle b/azure-client-runtime/build.gradle index 5c29359536abc..a65f3dc196dfe 100644 --- a/azure-client-runtime/build.gradle +++ b/azure-client-runtime/build.gradle @@ -10,7 +10,7 @@ buildscript { apply plugin: 'java' apply plugin: 'checkstyle' -version = '1.0.0-SNAPSHOT' +version = '1.0.0-beta2' checkstyle { toolVersion = "6.18" @@ -20,7 +20,7 @@ checkstyle { } dependencies { - compile 'com.microsoft.rest:client-runtime:1.0.0-SNAPSHOT' + compile 'com.microsoft.rest:client-runtime:1.0.0-beta2' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-runtime/pom.xml b/azure-client-runtime/pom.xml index 98d91b43e802e..9bc18f1f83d9f 100644 --- a/azure-client-runtime/pom.xml +++ b/azure-client-runtime/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-SNAPSHOT + 1.0.0-beta2 ../pom.xml diff --git a/build-tools/pom.xml b/build-tools/pom.xml index d540f8f5e6980..e94e7bf4f6ce1 100644 --- a/build-tools/pom.xml +++ b/build-tools/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-SNAPSHOT + 1.0.0-beta2 ../pom.xml diff --git a/client-runtime/build.gradle b/client-runtime/build.gradle index 73750c0d3d935..a633beff14bc3 100644 --- a/client-runtime/build.gradle +++ b/client-runtime/build.gradle @@ -12,7 +12,7 @@ apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'checkstyle' group = 'com.microsoft.rest' -version = '1.0.0-SNAPSHOT' +version = '1.0.0-beta2' checkstyle { toolVersion = "6.18" diff --git a/client-runtime/pom.xml b/client-runtime/pom.xml index e63a421434b38..b8d26493b263f 100644 --- a/client-runtime/pom.xml +++ b/client-runtime/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-SNAPSHOT + 1.0.0-beta2 ../pom.xml diff --git a/pom.xml b/pom.xml index aa77260c5893f..35c9c37b662c2 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-SNAPSHOT + 1.0.0-beta2 pom AutoRest Client Runtimes for Java @@ -140,7 +140,7 @@ com.microsoft.azure autorest-build-tools - 1.0.0-SNAPSHOT + 1.0.0-beta2 com.puppycrawl.tools From 34a1832d5d2e7321403c99616772baea91200fb7 Mon Sep 17 00:00:00 2001 From: Jianghao Lu Date: Tue, 12 Jul 2016 10:23:54 -0700 Subject: [PATCH 27/27] Prepare for next development iteration --- azure-android-client-authentication/build.gradle | 4 ++-- azure-client-authentication/build.gradle | 4 ++-- azure-client-authentication/pom.xml | 4 ++-- azure-client-runtime/build.gradle | 4 ++-- azure-client-runtime/pom.xml | 2 +- build-tools/pom.xml | 2 +- client-runtime/build.gradle | 2 +- client-runtime/pom.xml | 2 +- pom.xml | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/azure-android-client-authentication/build.gradle b/azure-android-client-authentication/build.gradle index c557f363b0093..2651d8389383e 100644 --- a/azure-android-client-authentication/build.gradle +++ b/azure-android-client-authentication/build.gradle @@ -18,7 +18,7 @@ android { minSdkVersion 15 targetSdkVersion 23 versionCode 1 - versionName "1.0.0-beta2" + versionName "1.0.0-SNAPSHOT" } buildTypes { @@ -51,7 +51,7 @@ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.0.1' compile 'com.microsoft.aad:adal:1.1.11' - compile 'com.microsoft.azure:azure-client-runtime:1.0.0-beta2' + compile 'com.microsoft.azure:azure-client-runtime:1.0.0-SNAPSHOT' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-authentication/build.gradle b/azure-client-authentication/build.gradle index f22be08ed8d0e..2b40ada004e0e 100644 --- a/azure-client-authentication/build.gradle +++ b/azure-client-authentication/build.gradle @@ -10,7 +10,7 @@ buildscript { apply plugin: 'java' apply plugin: 'checkstyle' -version = '1.0.0-beta2' +version = '1.0.0-SNAPSHOT' checkstyle { toolVersion = "6.18" @@ -21,7 +21,7 @@ checkstyle { dependencies { compile 'com.microsoft.azure:adal4j:1.1.2' - compile 'com.microsoft.azure:azure-client-runtime:1.0.0-beta2' + compile 'com.microsoft.azure:azure-client-runtime:1.0.0-SNAPSHOT' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-authentication/pom.xml b/azure-client-authentication/pom.xml index ecc305c3ecedd..47582ca1eaa87 100644 --- a/azure-client-authentication/pom.xml +++ b/azure-client-authentication/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-beta2 + 1.0.0-SNAPSHOT ../pom.xml @@ -49,7 +49,7 @@ com.microsoft.azure azure-client-runtime - 1.0.0-beta2 + 1.0.0-SNAPSHOT com.microsoft.azure diff --git a/azure-client-runtime/build.gradle b/azure-client-runtime/build.gradle index a65f3dc196dfe..5c29359536abc 100644 --- a/azure-client-runtime/build.gradle +++ b/azure-client-runtime/build.gradle @@ -10,7 +10,7 @@ buildscript { apply plugin: 'java' apply plugin: 'checkstyle' -version = '1.0.0-beta2' +version = '1.0.0-SNAPSHOT' checkstyle { toolVersion = "6.18" @@ -20,7 +20,7 @@ checkstyle { } dependencies { - compile 'com.microsoft.rest:client-runtime:1.0.0-beta2' + compile 'com.microsoft.rest:client-runtime:1.0.0-SNAPSHOT' testCompile 'junit:junit:4.12' testCompile 'junit:junit-dep:4.11' deployerJars "org.apache.maven.wagon:wagon-ftp:2.10" diff --git a/azure-client-runtime/pom.xml b/azure-client-runtime/pom.xml index 9bc18f1f83d9f..98d91b43e802e 100644 --- a/azure-client-runtime/pom.xml +++ b/azure-client-runtime/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-beta2 + 1.0.0-SNAPSHOT ../pom.xml diff --git a/build-tools/pom.xml b/build-tools/pom.xml index e94e7bf4f6ce1..d540f8f5e6980 100644 --- a/build-tools/pom.xml +++ b/build-tools/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-beta2 + 1.0.0-SNAPSHOT ../pom.xml diff --git a/client-runtime/build.gradle b/client-runtime/build.gradle index a633beff14bc3..73750c0d3d935 100644 --- a/client-runtime/build.gradle +++ b/client-runtime/build.gradle @@ -12,7 +12,7 @@ apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'checkstyle' group = 'com.microsoft.rest' -version = '1.0.0-beta2' +version = '1.0.0-SNAPSHOT' checkstyle { toolVersion = "6.18" diff --git a/client-runtime/pom.xml b/client-runtime/pom.xml index b8d26493b263f..e63a421434b38 100644 --- a/client-runtime/pom.xml +++ b/client-runtime/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-beta2 + 1.0.0-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 35c9c37b662c2..aa77260c5893f 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ com.microsoft.azure autorest-clientruntime-for-java - 1.0.0-beta2 + 1.0.0-SNAPSHOT pom AutoRest Client Runtimes for Java @@ -140,7 +140,7 @@ com.microsoft.azure autorest-build-tools - 1.0.0-beta2 + 1.0.0-SNAPSHOT com.puppycrawl.tools