Add item recommendation output with custom item attributes support
diff --git a/client/src/main/java/io/prediction/Client.java b/client/src/main/java/io/prediction/Client.java
index de01d8c..30bde8a 100644
--- a/client/src/main/java/io/prediction/Client.java
+++ b/client/src/main/java/io/prediction/Client.java
@@ -1,6 +1,7 @@
 package io.prediction;
 
 import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParseException;
 import com.google.gson.JsonParser;
@@ -11,6 +12,9 @@
 
 import java.io.IOException;
 import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.concurrent.ExecutionException;
 
 /**
@@ -22,7 +26,7 @@
  * Multiple simultaneous asynchronous requests is made possible by the high performance backend provided by the <a href="https://github.com/AsyncHttpClient/async-http-client">Async Http Client</a>.
  *
  * @author The PredictionIO Team (<a href="http://prediction.io">http://prediction.io</a>)
- * @version 0.4
+ * @version 0.4.1
  * @since 0.1
  */
 public class Client {
@@ -136,7 +140,12 @@
         int l = a.size();
         String[] r = new String[l];
         for (int i = 0; i < l; i++) {
-            r[i] = a.get(i).getAsString();
+            JsonElement e = a.get(i);
+            if (e.isJsonNull()) {
+                r[i] = null;
+            } else {
+                r[i] = e.getAsString();
+            }
         }
         return r;
     }
@@ -529,6 +538,18 @@
     }
 
     /**
+     * Get a get top-n recommendations request builder that can be used to add additional request parameters.
+     *
+     * @param engine engine name
+     * @param uid ID of the User whose recommendations will be gotten
+     * @param n number of top recommendations to get
+     * @param attributes array of item attribute names to be returned with the result
+     */
+    public ItemRecGetTopNRequestBuilder getItemRecGetTopNRequestBuilder(String engine, String uid, int n, String[] attributes) {
+        return (new ItemRecGetTopNRequestBuilder(this.apiUrl, this.apiFormat, this.appkey, engine, uid, n)).attributes(attributes);
+    }
+
+    /**
      * Sends an asynchronous get recommendations request to the API.
      *
      * @param builder an instance of {@link ItemRecGetTopNRequestBuilder} that will be turned into a request
@@ -588,6 +609,61 @@
         }
     }
 
+    /**
+     * Sends a synchronous get recommendations request to the API.
+     *
+     * @param engine engine name
+     * @param uid ID of the User whose recommendations will be gotten
+     * @param n number of top recommendations to get
+     * @param attributes array of item attribute names to be returned with the result
+     *
+     * @throws ExecutionException indicates an error in the HTTP backend
+     * @throws InterruptedException indicates an interruption during the HTTP operation
+     * @throws IOException indicates an error from the API response
+     */
+    public Map<String, String[]> getItemRecTopNWithAttributes(String engine, String uid, int n, String[] attributes) throws ExecutionException, InterruptedException, IOException {
+        return this.getItemRecTopNWithAttributes(this.getItemRecTopNAsFuture(this.getItemRecGetTopNRequestBuilder(engine, uid, n, attributes)));
+    }
+
+    /**
+     * Sends a synchronous get recommendations request to the API.
+     *
+     * @param builder an instance of {@link ItemRecGetTopNRequestBuilder} that will be turned into a request
+     *
+     * @throws ExecutionException indicates an error in the HTTP backend
+     * @throws InterruptedException indicates an interruption during the HTTP operation
+     * @throws IOException indicates an error from the API response
+     */
+    public Map<String, String[]> getItemRecTopNWithAttributes(ItemRecGetTopNRequestBuilder builder) throws ExecutionException, InterruptedException, IOException {
+        return this.getItemRecTopNWithAttributes(this.getItemRecTopNAsFuture(builder));
+    }
+
+    /**
+     * Synchronize a previously sent asynchronous get recommendations request.
+     *
+     * @param response an instance of {@link FutureAPIResponse} returned from {@link Client#getItemRecTopNAsFuture}
+     *
+     * @throws ExecutionException indicates an error in the HTTP backend
+     * @throws InterruptedException indicates an interruption during the HTTP operation
+     * @throws IOException indicates an error from the API response
+     */
+    public Map<String, String[]> getItemRecTopNWithAttributes(FutureAPIResponse response) throws ExecutionException, InterruptedException, IOException {
+        // Do not use getStatus/getMessage directly as they do not pass exceptions
+        int status = response.get().getStatus();
+        String message = response.get().getMessage();
+
+        if (status == Client.HTTP_OK) {
+            HashMap<String, String[]> results = new HashMap();
+            JsonObject messageAsJson = (JsonObject) parser.parse(message);
+            for (Map.Entry<String, JsonElement> member : messageAsJson.entrySet()) {
+                results.put(member.getKey(), this.jsonArrayAsStringArray(member.getValue().getAsJsonArray()));
+            }
+            return results;
+        } else {
+            throw new IOException(message);
+        }
+    }
+
     private void userActionItem(FutureAPIResponse response) throws ExecutionException, InterruptedException, IOException {
         int status = response.get().getStatus();
         String message = response.get().getMessage();
diff --git a/examples/androidclient/ivy.xml b/examples/androidclient/ivy.xml
index 30cc2d3..68c9fb5 100644
--- a/examples/androidclient/ivy.xml
+++ b/examples/androidclient/ivy.xml
@@ -17,7 +17,7 @@
         <dependency
             name="client"
             org="io.prediction"
-            rev="0.4" />
+            rev="0.4.1" />
     </dependencies>
 
 </ivy-module>
\ No newline at end of file
diff --git a/examples/androidclient/res/layout/activity_main.xml b/examples/androidclient/res/layout/activity_main.xml
index bbfc428..d2ed64b 100644
--- a/examples/androidclient/res/layout/activity_main.xml
+++ b/examples/androidclient/res/layout/activity_main.xml
@@ -11,7 +11,9 @@
         android:id="@+id/app_key"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:hint="@string/app_key" />
+        android:hint="@string/app_key"
+        android:lines="1"
+        android:singleLine="true" />
 
     <LinearLayout
         android:layout_width="match_parent"
@@ -23,7 +25,9 @@
             android:layout_height="wrap_content"
             android:layout_weight="1"
             android:hint="@string/api_url"
-            android:inputType="textUri" />
+            android:inputType="textUri"
+            android:lines="1"
+            android:singleLine="true" />
 
         <Button
             android:layout_width="wrap_content"
@@ -32,6 +36,11 @@
             android:text="@string/button_get_status" />
     </LinearLayout>
 
+    <View
+        android:layout_width="fill_parent"
+        android:layout_height="1dip"
+        android:background="#80000000" />
+
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content" >
@@ -41,21 +50,41 @@
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
-            android:hint="@string/engine" />
+            android:hint="@string/engine"
+            android:lines="1"
+            android:singleLine="true" />
 
         <EditText
             android:id="@+id/uid"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
-            android:hint="@string/uid" />
+            android:hint="@string/uid"
+            android:lines="1"
+            android:singleLine="true" />
 
         <EditText
             android:id="@+id/n"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:hint="@string/n"
-            android:inputType="number" />
+            android:inputType="number"
+            android:lines="1"
+            android:singleLine="true" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" >
+
+        <EditText
+            android:id="@+id/attributes"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:hint="@string/attributes"
+            android:lines="1"
+            android:singleLine="true" />
 
         <Button
             android:layout_width="wrap_content"
@@ -64,6 +93,11 @@
             android:text="@string/button_get_recs" />
     </LinearLayout>
 
+    <View
+        android:layout_width="fill_parent"
+        android:layout_height="1dip"
+        android:background="#80000000" />
+
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content" >
@@ -73,14 +107,18 @@
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
-            android:hint="@string/uid" />
+            android:hint="@string/uid"
+            android:lines="1"
+            android:singleLine="true" />
 
         <EditText
             android:id="@+id/view_iid"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
-            android:hint="@string/iid" />
+            android:hint="@string/iid"
+            android:lines="1"
+            android:singleLine="true" />
 
         <Button
             android:layout_width="wrap_content"
@@ -89,6 +127,11 @@
             android:text="@string/button_save_view" />
     </LinearLayout>
 
+    <View
+        android:layout_width="fill_parent"
+        android:layout_height="1dip"
+        android:background="#80000000" />
+
     <TextView
         android:id="@+id/openudid"
         android:layout_width="match_parent"
@@ -96,6 +139,11 @@
         android:hint="@string/openudid"
         android:textIsSelectable="true" />
 
+    <View
+        android:layout_width="fill_parent"
+        android:layout_height="1dip"
+        android:background="#80000000" />
+
     <TextView
         android:id="@+id/console_output"
         android:layout_width="match_parent"
diff --git a/examples/androidclient/res/values/strings.xml b/examples/androidclient/res/values/strings.xml
index bc2872f..7235d6c 100644
--- a/examples/androidclient/res/values/strings.xml
+++ b/examples/androidclient/res/values/strings.xml
@@ -12,6 +12,7 @@
     <string name="uid">UID</string>
     <string name="iid">IID</string>
     <string name="n">#</string>
+    <string name="attributes">Item Attributes</string>
     <string name="openudid">OpenUDID</string>
     
 </resources>
\ No newline at end of file
diff --git a/examples/androidclient/src/io/prediction/samples/androidclient/MainActivity.java b/examples/androidclient/src/io/prediction/samples/androidclient/MainActivity.java
index 9c4a391..cc67a76 100644
--- a/examples/androidclient/src/io/prediction/samples/androidclient/MainActivity.java
+++ b/examples/androidclient/src/io/prediction/samples/androidclient/MainActivity.java
@@ -1,6 +1,10 @@
 package io.prediction.samples.androidclient;
 
 import io.prediction.Client;
+import io.prediction.ItemRecGetTopNRequestBuilder;
+
+import java.util.HashMap;
+import java.util.Map;
 
 import org.OpenUDID.OpenUDID_manager;
 import org.apache.commons.lang3.StringUtils;
@@ -43,30 +47,47 @@
 	}
 
 	// Get recommendations async task
-	private class RecsTask extends AsyncTask<Void, Void, String> {
-		protected String doInBackground(Void... v) {
+	private class RecsTask extends AsyncTask<Void, Void, Map<String, String[]>> {
+		protected Map<String, String[]> doInBackground(Void... v) {
 			EditText appKey = (EditText) findViewById(R.id.app_key);
 			EditText apiUrl = (EditText) findViewById(R.id.api_url);
 			EditText engine = (EditText) findViewById(R.id.engine);
 			EditText uid = (EditText) findViewById(R.id.uid);
 			EditText n = (EditText) findViewById(R.id.n);
+			EditText attributes = (EditText) findViewById(R.id.attributes);
 			client.setAppkey(appKey.getText().toString());
 			client.setApiUrl(apiUrl.getText().toString());
-			String result = "";
+			Map<String, String[]> results = new HashMap<String, String[]>();
 			try {
-				String[] iids = client.getItemRecTopN(engine.getText()
-						.toString(), uid.getText().toString(), Integer
-						.parseInt(n.getText().toString()));
-				result = StringUtils.join(iids, ",");
+				// Use a builder to insert optional parameter
+				ItemRecGetTopNRequestBuilder builder = client
+						.getItemRecGetTopNRequestBuilder(engine.getText()
+								.toString(), uid.getText().toString(), Integer
+								.parseInt(n.getText().toString()));
+
+				if (!isEmpty(attributes)) {
+					// Include custom attributes in results
+					String[] attributesToGet = attributes.getText().toString()
+							.split(",");
+					builder.attributes(attributesToGet);
+					results = client.getItemRecTopNWithAttributes(builder);
+				} else {
+					results.put("iids", client.getItemRecTopN(builder));
+				}
 			} catch (Exception e) {
-				result = ExceptionUtils.getStackTrace(e);
+				String[] error = { ExceptionUtils.getStackTrace(e) };
+				results.put("error", error);
 			}
-			return result;
+			return results;
 		}
 
-		protected void onPostExecute(String result) {
+		protected void onPostExecute(Map<String, String[]> result) {
 			TextView console = (TextView) findViewById(R.id.console_output);
-			console.setText(result);
+			String display = "";
+			for (Map.Entry<String, String[]> entry : result.entrySet()) {
+				display += entry.getKey() + ": " + StringUtils.join(entry.getValue(), ",") + "\n";
+			}
+			console.setText(display);
 		}
 	}
 
@@ -84,7 +105,7 @@
 				String suid = uid.getText().toString();
 				String siid = iid.getText().toString();
 				client.userViewItem(suid, siid);
-				result = "Logged UID "+suid+" view IID "+siid;
+				result = "Logged UID " + suid + " view IID " + siid;
 			} catch (Exception e) {
 				result = ExceptionUtils.getStackTrace(e);
 			}
@@ -112,7 +133,7 @@
 
 		protected void onPostExecute(String result) {
 			TextView console = (TextView) findViewById(R.id.openudid);
-			console.setText("OpenUDID: "+result);
+			console.setText("OpenUDID: " + result);
 		}
 	}
 
@@ -170,4 +191,11 @@
 		new SaveViewTask().execute();
 	}
 
+	private boolean isEmpty(EditText etText) {
+		if (etText.getText().toString().trim().length() > 0) {
+			return false;
+		} else {
+			return true;
+		}
+	}
 }