Adventures in Java: RESTful client (Android)

A RESTful Java client for Android (and more)

So recently I’ve been working on an Android application and a RESTful server application written in Java. On the server side I chose to leverage Jersey because it seemed the easiest to get going on, there was already support build into Netbeans, and there is plenty of documentation and support for it out on the internet. On the client side for the Android app, I also decided to use Jersey for my framework, at least at first.

After writing some test cases and validating my service endpoints in a test project, I began moving the code into my Android application. I got my rest client setup, created all my helper classes, and began testing. Everything appeared to be working as expected until I tried to do a POST call to the web service and problems began to surface. This came up specifically when trying to do a multipart form post through the service client, however, I came to find out there are some significant underlying issues with Jersey and Android. Although I was able to get the client to work by implementing ServiceIteratorProvider class, as referenced in this Stackoverflow posting, and calling it before creating the Client object. This fixed many of the issues, but Android was still throwing some random stacktraces when the client was called, so in the end I decided to roll out a solution based on Apache HttpComponents. While Android does include a version of Apache HttpComponents, I added the latest version with some supporting libraries such as GSON for handling some JSON parsing from the web service.

I wanted to create a client architecture that was reusable not only in the Android application, but some non-android applications that I was planning to write in the near future. After a day of tooling around, the following is what I came up with. This is still a work in progress, but so far it’s working out well.


/*
 * Copyright (C) 2013 Russell Shingleton.
 *
 * 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 org.groundworkgroup.capoc.webservice;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Date;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.groundworkgroup.util.Base64;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.stream.JsonReader;

public class RestClient {

	public enum RequestMethod {
		DELETE, GET, POST, PUT
	}

	//	Collections
	private ArrayList headers;
	private ArrayList params;

	//	HTTP Stuff
	private HttpUriRequest request;
	private HttpClient client;
	private HttpResponse response;
	private MultipartEntity multipartEntity;
	private RequestMethod method;

	//	HTTP Basic Authentication
	private String password;
	private String username;

	//	Misc
	private Gson gson;
	private String authCode;
	private String responseBody;
	private String url;
	private boolean authentication;
	private boolean multipart;
	private int status;

	public RestClient(String url) {
		this.url = url;
		this.headers = new ArrayList();
		this.params = new ArrayList();
		this.client = new DefaultHttpClient();
	}

	public void addBasicAuthentication(String user, String pass) {
		authentication = true;
		username = user;
		password = pass;
		authCode = new String(Base64.encodeBytes((username + ":" + password).getBytes()));
	}

	public void addHeader(String name, String value) {
		headers.add(new BasicNameValuePair(name, value));
	}

	public void addParameter(String name, String value) {
		params.add(new BasicNameValuePair(name, value));
	}

	public void addMutlipartFile(String name, File file) {
		initMultipart();
		multipartEntity.addPart(name, new FileBody(file));
	}

	public void addMultipartField(String name, String value) {
		initMultipart();
		try {
			multipartEntity.addPart(name, new StringBody(value));
		} catch (UnsupportedEncodingException e) {
			// TODO Update logging
			e.printStackTrace();
		}
	}

	private void initMultipart() {
		multipart = true;
		if (multipartEntity == null) {
			multipartEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
		}
	}

	public void addAcceptContentType(ContentType type) {
		addHeader("Accept", type.getMimeType());
	}

	public void addRequestContentType(ContentType type) {
		addHeader("Content-type", type.getMimeType());
	}

	public HttpResponse execute(RequestMethod method) throws Exception {
		this.method = method;

		// Add any parameters to the url before creating the request object
		String uri = addParameters(url);

		// Create the appropriate request object based on the method
		switch (method) {
			case GET:
				request = new HttpGet(uri);
				break;

			case POST:
				request = new HttpPost(uri);
				break;

			case PUT:
				request = new HttpPut(uri);
				break;

			case DELETE:
				request = new HttpDelete(uri);
		}

		return processRequest();
	}

	public HttpResponse processRequest() {
		addHeaders();
		try {
			switch (method) {
				case POST:
					if (multipart) {
						((HttpPost) request).setEntity(multipartEntity);
					}

					break;
				default:
					break;
			}

			response = client.execute(request);
			status = response.getStatusLine().getStatusCode();

		} catch (ClientProtocolException e) {
			// TODO Update logging
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Update logging
			e.printStackTrace();
		}
		return response;
	}

	private void addHeaders() {
		for (NameValuePair h : headers) {
			request.addHeader(h.getName(), h.getValue());
		}

		if (authentication) {
			request.addHeader("Authorization", "BASIC " + authCode);
		}
	}

	private String addParameters(String uri) {
		StringBuilder url = new StringBuilder(uri);
		if (!params.isEmpty()) {
			if (!uri.endsWith("?")) {
				url.append("?");
			}

			url.append(URLEncodedUtils.format(params, "utf-8"));
			return url.toString();
		}
		return uri;
	}

	@SuppressWarnings("unchecked")
	public  T convertToJsonObject(Class<?> classOfT) throws UnsupportedEncodingException,
			IllegalStateException, IOException {
		gson = createGson();
		return (T) gson.fromJson(getJsonReader(), classOfT);
	}

	public  T convertToJsonObject(Type typeOfT) throws UnsupportedEncodingException,
			IllegalStateException, IOException {
		gson = createGson();
		return gson.fromJson(getJsonReader(), typeOfT);
	}

	private Gson createGson() {
		// Register an adapter to manage the date types as long values
		return new GsonBuilder().registerTypeAdapter(Date.class, new JsonDeserializer() {
			public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
					throws JsonParseException {
				return new Date(json.getAsJsonPrimitive().getAsLong());
			}
		}).create();
	}

	private JsonReader getJsonReader() throws UnsupportedEncodingException, IllegalStateException, IOException {
		return new JsonReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
	}

	private String convertResponseToString() throws ParseException, IOException {
		InputStream is = response.getEntity().getContent();
		BufferedReader reader = new BufferedReader(new InputStreamReader(is));
		StringBuilder sb = new StringBuilder();

		String line = null;
		try {
			while ((line = reader.readLine()) != null) {
				sb.append((line + "\n"));
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				is.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return sb.toString();
	}

	public String getResponseAsString() throws ParseException, IOException {
		if (responseBody == null) {
			responseBody = convertResponseToString();
		}
		return responseBody;
	}

	public int getStatus() {
		return status;
	}

}