From e4e925403413601f092f1c8830d722e17f41d178 Mon Sep 17 00:00:00 2001 From: Inderjeet Singh Date: Wed, 20 Jan 2010 13:59:59 +0000 Subject: [PATCH] Added an asynchronous client for invoking Web-services. This client uses a queue to hold web-service requests and returns the control back to the caller. A separate thread executes these web-service requests and invokes a caller-suplied callback with the results. Made WebServiceClient reusable across different type of Web-service calls. Added configurable logging support in RequestSender, ResponseReceiver and WebServiceClients. Added some logging. Throwing WebServiceSystemException instead of RuntimException. --- .../gson/webservice/client/QueueConsumer.java | 59 ++++++++++++++++ .../gson/webservice/client/QueueEntry.java | 38 ++++++++++ .../gson/webservice/client/RequestSender.java | 15 +++- .../webservice/client/ResponseCallback.java | 33 +++++++++ .../webservice/client/ResponseReceiver.java | 12 ++++ .../client/SingleThreadExecutor.java | 37 ++++++++++ .../gson/webservice/client/TaskExecutor.java | 29 ++++++++ .../client/ThreadPerTaskExecutor.java | 36 ++++++++++ .../webservice/client/WebServiceClient.java | 46 ++++++++---- .../client/WebServiceClientAsync.java | 70 +++++++++++++++++++ 10 files changed, 360 insertions(+), 15 deletions(-) create mode 100644 wsclient/src/main/java/com/google/gson/webservice/client/QueueConsumer.java create mode 100644 wsclient/src/main/java/com/google/gson/webservice/client/QueueEntry.java create mode 100644 wsclient/src/main/java/com/google/gson/webservice/client/ResponseCallback.java create mode 100644 wsclient/src/main/java/com/google/gson/webservice/client/SingleThreadExecutor.java create mode 100644 wsclient/src/main/java/com/google/gson/webservice/client/TaskExecutor.java create mode 100644 wsclient/src/main/java/com/google/gson/webservice/client/ThreadPerTaskExecutor.java create mode 100644 wsclient/src/main/java/com/google/gson/webservice/client/WebServiceClientAsync.java diff --git a/wsclient/src/main/java/com/google/gson/webservice/client/QueueConsumer.java b/wsclient/src/main/java/com/google/gson/webservice/client/QueueConsumer.java new file mode 100644 index 00000000..fe50883b --- /dev/null +++ b/wsclient/src/main/java/com/google/gson/webservice/client/QueueConsumer.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gson.webservice.client; + +import java.util.concurrent.BlockingQueue; + +import com.google.gson.webservice.definition.WebServiceCall; +import com.google.gson.webservice.definition.WebServiceResponse; +import com.google.gson.webservice.definition.WebServiceSystemException; + +/** + * A consumer that executes in its own thread consuming queue entries and invoking web-service calls + * + * @author inder + */ +final class QueueConsumer implements Runnable { + + private final BlockingQueue queue; + private WebServiceClient client; + + QueueConsumer(BlockingQueue queue, WebServiceClient client) { + this.queue = queue; + this.client = client; + } + + @Override + public void run() { + try { + while(true) { + consume(queue.take()); + } + } catch (InterruptedException e) { + // exit + } + } + + private void consume(QueueEntry entry) { + try { + WebServiceResponse response = client.getResponse(entry.callSpec, entry.request); + WebServiceCall call = new WebServiceCall(entry.callSpec, entry.request, response); + entry.responseCallback.handleResponse(call); + } catch (WebServiceSystemException e) { + entry.responseCallback.handleError(e, entry.request, entry.callSpec); + } + } +} diff --git a/wsclient/src/main/java/com/google/gson/webservice/client/QueueEntry.java b/wsclient/src/main/java/com/google/gson/webservice/client/QueueEntry.java new file mode 100644 index 00000000..01ca4c80 --- /dev/null +++ b/wsclient/src/main/java/com/google/gson/webservice/client/QueueEntry.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gson.webservice.client; + +import com.google.gson.webservice.definition.WebServiceCallSpec; +import com.google.gson.webservice.definition.WebServiceRequest; + +/** + * A holder class for an entry stored in queue. It contains references to the request, callspec, + * and the client-supplied callback to provide sufficient information to execute a web-service call. + * + * @author inder + */ +final class QueueEntry { + final WebServiceCallSpec callSpec; + final WebServiceRequest request; + final ResponseCallback responseCallback; + + QueueEntry(WebServiceCallSpec callSpec, WebServiceRequest request, + ResponseCallback responseCallback) { + this.callSpec = callSpec; + this.request = request; + this.responseCallback = responseCallback; + } +} diff --git a/wsclient/src/main/java/com/google/gson/webservice/client/RequestSender.java b/wsclient/src/main/java/com/google/gson/webservice/client/RequestSender.java index 8b554d90..c203b45c 100644 --- a/wsclient/src/main/java/com/google/gson/webservice/client/RequestSender.java +++ b/wsclient/src/main/java/com/google/gson/webservice/client/RequestSender.java @@ -21,12 +21,15 @@ import java.io.Writer; import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import com.google.gson.Gson; import com.google.gson.webservice.definition.HeaderMap; import com.google.gson.webservice.definition.HeaderMapSpec; import com.google.gson.webservice.definition.RequestBody; import com.google.gson.webservice.definition.WebServiceRequest; +import com.google.gson.webservice.definition.WebServiceSystemException; /** * Class to send Web service requests on a {@link HttpURLConnection}. @@ -35,9 +38,16 @@ import com.google.gson.webservice.definition.WebServiceRequest; */ public final class RequestSender { private final Gson gson; + private final Logger logger; + private final Level logLevel; public RequestSender(Gson gson) { + this(gson, null); + } + public RequestSender(Gson gson, Level logLevel) { this.gson = gson; + logger = logLevel == null ? null : Logger.getLogger(RequestSender.class.getName()); + this.logLevel = logLevel; } public void send(HttpURLConnection conn, WebServiceRequest request) { @@ -60,7 +70,7 @@ public final class RequestSender { // Initiate the sending of the request. conn.connect(); } catch (IOException e) { - throw new RuntimeException(e); + throw new WebServiceSystemException(e); } } @@ -72,6 +82,9 @@ public final class RequestSender { Object value = entry.getValue(); String json = gson.toJson(value, type); conn.addRequestProperty(paramName, json); + if (logger != null) { + logger.log(logLevel, String.format("Request param: %s:%s", paramName, json)); + } } } diff --git a/wsclient/src/main/java/com/google/gson/webservice/client/ResponseCallback.java b/wsclient/src/main/java/com/google/gson/webservice/client/ResponseCallback.java new file mode 100644 index 00000000..61989694 --- /dev/null +++ b/wsclient/src/main/java/com/google/gson/webservice/client/ResponseCallback.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gson.webservice.client; + +import com.google.gson.webservice.definition.WebServiceCall; +import com.google.gson.webservice.definition.WebServiceCallSpec; +import com.google.gson.webservice.definition.WebServiceRequest; +import com.google.gson.webservice.definition.WebServiceSystemException; + +/** + * A client-supplied callback to be used with {@link WebServiceClientAsync}. When a web-service + * call is executed asynchronously, this callback is invoked with the results. + * + * @author inder + */ +public interface ResponseCallback { + public void handleResponse(WebServiceCall call); + public void handleError(WebServiceSystemException e, WebServiceRequest request, + WebServiceCallSpec callSpec); +} diff --git a/wsclient/src/main/java/com/google/gson/webservice/client/ResponseReceiver.java b/wsclient/src/main/java/com/google/gson/webservice/client/ResponseReceiver.java index 7852ac53..811722ff 100644 --- a/wsclient/src/main/java/com/google/gson/webservice/client/ResponseReceiver.java +++ b/wsclient/src/main/java/com/google/gson/webservice/client/ResponseReceiver.java @@ -22,6 +22,8 @@ import java.io.Reader; import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import com.google.gson.Gson; import com.google.gson.webservice.definition.HeaderMap; @@ -39,10 +41,17 @@ import com.google.gson.webservice.definition.WebServiceResponse; public final class ResponseReceiver { private final Gson gson; private final ResponseSpec spec; + private final Logger logger; + private final Level logLevel; public ResponseReceiver(Gson gson, ResponseSpec spec) { + this(gson, spec, null); + } + public ResponseReceiver(Gson gson, ResponseSpec spec, Level logLevel) { this.gson = gson; this.spec = spec; + this.logger = logLevel == null ? null : Logger.getLogger(ResponseReceiver.class.getName()); + this.logLevel = logLevel; } public WebServiceResponse receive(HttpURLConnection conn) { @@ -64,6 +73,9 @@ public final class ResponseReceiver { String paramName = entry.getKey(); String json = conn.getHeaderField(paramName); if (json != null) { + if (logger != null) { + logger.log(logLevel, String.format("Response Header: %s:%s\n", paramName, json)); + } Type typeOfT = paramsSpec.getTypeFor(paramName); Object value = gson.fromJson(json, typeOfT); paramsBuilder.put(paramName, value, typeOfT); diff --git a/wsclient/src/main/java/com/google/gson/webservice/client/SingleThreadExecutor.java b/wsclient/src/main/java/com/google/gson/webservice/client/SingleThreadExecutor.java new file mode 100644 index 00000000..100751df --- /dev/null +++ b/wsclient/src/main/java/com/google/gson/webservice/client/SingleThreadExecutor.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gson.webservice.client; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * An executor that uses a single thread to execute all calls + * + * @author inder + */ +final class SingleThreadExecutor implements TaskExecutor { + private ExecutorService executor; + public void execute(final Runnable r) { + executor = Executors.newSingleThreadExecutor(); + executor.execute(r); + } + + @Override + public void shutdownNow() { + executor.shutdownNow(); + } +} \ No newline at end of file diff --git a/wsclient/src/main/java/com/google/gson/webservice/client/TaskExecutor.java b/wsclient/src/main/java/com/google/gson/webservice/client/TaskExecutor.java new file mode 100644 index 00000000..94a7de5d --- /dev/null +++ b/wsclient/src/main/java/com/google/gson/webservice/client/TaskExecutor.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gson.webservice.client; + +import java.util.concurrent.Executor; + +/** + * An {@link Executor} with an additional method for shutdown. We could have just used + * {@link java.util.concurent.ExecutorService}, however, that requires too many methods to be + * implemented. + * + * @author inder + */ +interface TaskExecutor extends Executor { + public void shutdownNow(); +} diff --git a/wsclient/src/main/java/com/google/gson/webservice/client/ThreadPerTaskExecutor.java b/wsclient/src/main/java/com/google/gson/webservice/client/ThreadPerTaskExecutor.java new file mode 100644 index 00000000..2682a4ca --- /dev/null +++ b/wsclient/src/main/java/com/google/gson/webservice/client/ThreadPerTaskExecutor.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gson.webservice.client; + +/** + * An executor that uses a new thread to execute each call + * + * @author inder + */ +final class ThreadPerTaskExecutor implements TaskExecutor { + private Thread thread; + public void execute(final Runnable r) { + thread = new Thread(r); + thread.start(); + } + + @Override + public void shutdownNow() { + if (thread != null) { + thread.interrupt(); + } + } +} \ No newline at end of file diff --git a/wsclient/src/main/java/com/google/gson/webservice/client/WebServiceClient.java b/wsclient/src/main/java/com/google/gson/webservice/client/WebServiceClient.java index 515941b3..31485d70 100644 --- a/wsclient/src/main/java/com/google/gson/webservice/client/WebServiceClient.java +++ b/wsclient/src/main/java/com/google/gson/webservice/client/WebServiceClient.java @@ -19,11 +19,17 @@ import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; +import java.util.logging.Level; +import java.util.logging.Logger; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.webservice.definition.ResponseBody; import com.google.gson.webservice.definition.WebServiceCallSpec; import com.google.gson.webservice.definition.WebServiceRequest; import com.google.gson.webservice.definition.WebServiceResponse; +import com.google.gson.webservice.definition.WebServiceSystemException; +import com.google.gson.webservice.typeadapters.ResponseBodyGsonConverter; /** * Main class used by clients to access a Gson Web service. @@ -32,16 +38,20 @@ import com.google.gson.webservice.definition.WebServiceResponse; */ public final class WebServiceClient { private final WebServiceConfig config; - private final WebServiceCallSpec callSpec; - private final Gson gson; + private final Logger logger; + private final Level logLevel; - public WebServiceClient(Gson gson, WebServiceConfig serverConfig, WebServiceCallSpec callSpec) { - this.gson = gson; + public WebServiceClient(WebServiceConfig serverConfig) { + this(serverConfig, null); + } + + public WebServiceClient(WebServiceConfig serverConfig, Level logLevel) { this.config = serverConfig; - this.callSpec = callSpec; + this.logger = logLevel == null ? null : Logger.getLogger(WebServiceClient.class.getName()); + this.logLevel = logLevel; } - private URL getWebServiceUrl() { + private URL getWebServiceUrl(WebServiceCallSpec callSpec) { String url = config.getServiceBaseUrl() + callSpec.getPath().get(); try { return new URL(url); @@ -50,23 +60,31 @@ public final class WebServiceClient { } } - public WebServiceResponse getResponse(WebServiceRequest request) { + public WebServiceResponse getResponse(WebServiceCallSpec callSpec, WebServiceRequest request) { try { - HttpURLConnection conn = (HttpURLConnection) getWebServiceUrl().openConnection(); + URL webServiceUrl = getWebServiceUrl(callSpec); + if (logger != null) { + logger.log(logLevel, "Opening connection to " + webServiceUrl); + } + HttpURLConnection conn = (HttpURLConnection) webServiceUrl.openConnection(); + Gson gson = new GsonBuilder() + .registerTypeAdapter(ResponseBody.class, + new ResponseBodyGsonConverter(callSpec.getResponseSpec().getBodySpec())) + .create(); RequestSender requestSender = new RequestSender(gson); requestSender.send(conn, request); - ResponseReceiver responseReceiver = new ResponseReceiver(gson, callSpec.getResponseSpec()); + ResponseReceiver responseReceiver = + new ResponseReceiver(gson, callSpec.getResponseSpec(), logLevel); return responseReceiver.receive(conn); } catch (IOException e) { - throw new RuntimeException(e); + throw new WebServiceSystemException(e); + } catch (IllegalArgumentException e) { + throw new WebServiceSystemException(e); } } @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{config:").append(config).append(",callSpec:").append(callSpec); - sb.append("gson:").append(gson).append("}"); - return sb.toString(); + return String.format("config:%s", config); } } diff --git a/wsclient/src/main/java/com/google/gson/webservice/client/WebServiceClientAsync.java b/wsclient/src/main/java/com/google/gson/webservice/client/WebServiceClientAsync.java new file mode 100644 index 00000000..efb46e1d --- /dev/null +++ b/wsclient/src/main/java/com/google/gson/webservice/client/WebServiceClientAsync.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gson.webservice.client; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Level; + +import com.google.gson.webservice.definition.WebServiceCallSpec; +import com.google.gson.webservice.definition.WebServiceRequest; +import com.google.gson.webservice.definition.WebServiceSystemException; + +/** + * A client for invoking a JSON-based Web-service in an asynchronous manner. The call is queued, + * and control returns to the caller. A separate thread executes the call, and invokes the + * client-supplied callback with results. + * + * @author inder + */ +public final class WebServiceClientAsync { + + private final BlockingQueue queue; + private final boolean threadPerTask; + private final TaskExecutor executor; + + public WebServiceClientAsync(WebServiceConfig serverConfig) { + this(serverConfig, null); + } + public WebServiceClientAsync(WebServiceConfig serverConfig, Level logLevel) { + this(new WebServiceClient(serverConfig, logLevel)); + } + + public WebServiceClientAsync(WebServiceClient client) { + queue = new LinkedBlockingQueue(); + this.threadPerTask = true; + QueueConsumer consumer = new QueueConsumer(queue, client); + executor = getExecutor(); + executor.execute(consumer); + } + + private TaskExecutor getExecutor() { + return threadPerTask ? new ThreadPerTaskExecutor() : new SingleThreadExecutor(); + } + + public void callAsync(WebServiceCallSpec callSpec, WebServiceRequest request, + ResponseCallback responseCallback) { + try { + queue.put(new QueueEntry(callSpec, request, responseCallback)); + } catch (InterruptedException e) { + throw new WebServiceSystemException(e); + } + } + + public void shutdownNow() { + executor.shutdownNow(); + } +}