Merge branch 'master' of https://github.com/PredictionIO/PredictionIO-Java-SDK
diff --git a/README.md b/README.md
index 88d0d81..8fadd34 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
         <dependency>
             <groupId>io.prediction</groupId>
             <artifactId>client</artifactId>
-            <version>0.8.3</version>
+            <version>0.9.5</version>
         </dependency>
     </dependencies>
     ...
@@ -36,7 +36,7 @@
 <ivy-module ...>
     ...
     <dependencies>
-        <dependency org="io.prediction" name="client" rev="0.8.3" />
+        <dependency org="io.prediction" name="client" rev="0.9.5" />
         ...
     </dependencies>
     ...
@@ -49,7 +49,7 @@
 If you have an sbt project, add the library dependency to your build definition.
 
 ```Scala
-libraryDependencies += "io.prediction" % "client" % "0.8.3"
+libraryDependencies += "io.prediction" % "client" % "0.9.5"
 ```
 
 
@@ -95,7 +95,7 @@
         <dependency>
             <groupId>io.prediction</groupId>
             <artifactId>client</artifactId>
-            <version>0.8.4-SNAPSHOT</version>
+            <version>0.9.6-SNAPSHOT</version>
         </dependency>
     </dependencies>
     ...
diff --git a/client/pom.xml b/client/pom.xml
index d0b2b41..e844bfb 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -5,7 +5,7 @@
     <groupId>io.prediction</groupId>
     <artifactId>sdk</artifactId>
     <relativePath>../pom.xml</relativePath>
-    <version>0.8.3</version>
+    <version>0.9.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>client</artifactId>
diff --git a/client/src/main/java/io/prediction/EventClient.java b/client/src/main/java/io/prediction/EventClient.java
index 8086328..1f42926 100644
--- a/client/src/main/java/io/prediction/EventClient.java
+++ b/client/src/main/java/io/prediction/EventClient.java
@@ -5,6 +5,8 @@
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonObject;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonArray;
 import com.ning.http.client.Request;
 import com.ning.http.client.RequestBuilder;
 
@@ -12,6 +14,7 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.LinkedList;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
 
@@ -151,6 +154,68 @@
     }
 
     /**
+     * Sends an asynchronous create events (batch) request to the API.
+     *
+     * @param events a List of {@link Event} that will be turned into a request
+     */
+    public FutureAPIResponse createEventsAsFuture(List<Event> events) throws IOException {
+        RequestBuilder builder = new RequestBuilder("POST");
+        builder.setUrl(apiUrl + "/batch/events.json?accessKey=" + accessKey);
+
+        GsonBuilder gsonBuilder = new GsonBuilder();
+        gsonBuilder.registerTypeAdapter(DateTime.class, new DateTimeAdapter());
+        Gson gson = gsonBuilder.create();
+        String requestJsonString = gson.toJson(events);
+
+        builder.setBody(requestJsonString);
+        builder.setHeader("Content-Type","application/json");
+        builder.setHeader("Content-Length", ""+requestJsonString.length());
+        return new FutureAPIResponse(client.executeRequest(builder.build(), getHandler()));
+    }
+
+    /**
+     * Sends a synchronous create events (batch) request to the API.
+     *
+     * @param events a List of {@link Event} that will be turned into a request
+     * @return event ID from the server
+     *
+     * @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 List<String> createEvents(List<Event> event)
+            throws ExecutionException, InterruptedException, IOException {
+        return createEvents(createEventsAsFuture(event));
+    }
+
+    /**
+     * Synchronize a previously sent asynchronous create events (batch) request.
+     *
+     * @param response an instance of {@link FutureAPIResponse} returned from
+     * {@link #createEventAsFuture}
+     * @return List of event IDs from the server
+     *
+     * @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 List<String> createEvents(FutureAPIResponse response)
+            throws ExecutionException, InterruptedException, IOException {
+        int status = response.get().getStatus();
+        String message = response.get().getMessage();
+
+        if (status != BaseClient.HTTP_OK) {
+            throw new IOException(status + " " + message);
+        }
+       List<String> eventIds = new LinkedList<String>();
+ 
+       for(JsonElement elem: (JsonArray)parser.parse(message) ){
+           eventIds.add(((JsonObject)elem).get("eventId").getAsString());
+       }
+       return eventIds;
+    }
+
+    /**
      * Sends an asynchronous get event request to the API.
      *
      * @param eid ID of the event to get
diff --git a/client/src/main/java/io/prediction/FileExporter.java b/client/src/main/java/io/prediction/FileExporter.java
new file mode 100644
index 0000000..7919ee9
--- /dev/null
+++ b/client/src/main/java/io/prediction/FileExporter.java
@@ -0,0 +1,64 @@
+package io.prediction;
+
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Map;
+import org.joda.time.DateTime;
+
+public class FileExporter {
+
+    private FileOutputStream out;
+
+    public FileExporter(String pathname) throws FileNotFoundException {
+        out = new FileOutputStream(pathname);
+    }
+
+    /**
+     * Create and write a json-encoded event to the underlying file.
+     *
+     * @param eventName        Name of the event.
+     * @param entityType       The entity type.
+     * @param entityId         The entity ID.
+     * @param targetEntityType The target entity type (optional).
+     * @param targetEntityId   The target entity ID (optional).
+     * @param properties       Properties (optional).
+     * @param eventTime        The time of the event (optional).
+     * @throws IOException
+     */
+    public void createEvent(String eventName, String entityType, String entityId,
+                            String targetEntityType, String targetEntityId, Map<String, Object> properties,
+                            DateTime eventTime) throws IOException {
+
+        if (eventTime == null) {
+            eventTime = new DateTime();
+        }
+
+        Event event = new Event()
+                .event(eventName)
+                .entityType(entityType)
+                .entityId(entityId)
+                .eventTime(eventTime);
+
+        if (targetEntityType != null) {
+            event.targetEntityType(targetEntityType);
+        }
+
+        if (targetEntityId != null) {
+            event.targetEntityId(targetEntityId);
+        }
+
+        if (properties != null) {
+            event.properties(properties);
+        }
+
+        out.write(event.toJsonString().getBytes("UTF8"));
+        out.write('\n');
+    }
+
+    public void close() throws IOException {
+        out.close();
+    }
+
+}
diff --git a/client/src/test/java/io/prediction/FileExporterTest.java b/client/src/test/java/io/prediction/FileExporterTest.java
new file mode 100644
index 0000000..a5c7759
--- /dev/null
+++ b/client/src/test/java/io/prediction/FileExporterTest.java
@@ -0,0 +1,116 @@
+package io.prediction;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.joda.time.DateTime;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import static org.junit.Assert.*;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class FileExporterTest {
+
+    @Rule
+    public TemporaryFolder folder = new TemporaryFolder();
+
+    @Test
+    public void testIt() throws IOException {
+
+        String pathname = folder.getRoot().getCanonicalPath() + "/testIt.out";
+
+        FileExporter exporter = new FileExporter(pathname);
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put("birthday", new DateTime("1758-05-06T00:00:00+00:00"));
+
+        DateTime then = new DateTime("1794-07-27T00:00:00+00:00");
+
+        exporter.createEvent("event-1", "entity-type-1", "entity-id-1",
+                null, null, null, null);
+
+        exporter.createEvent("event-2", "entity-type-2", "entity-id-2",
+                "target-entity-type-2", null, null, null);
+
+        exporter.createEvent("event-3", "entity-type-3", "entity-id-3",
+                null, "target-entity-id-3", null, null);
+
+        exporter.createEvent("event-4", "entity-type-4", "entity-id-4",
+                null, null, properties, then);
+
+        exporter.createEvent("event-5", "entity-type-5", "entity-id-5",
+                "target-entity-type-5", "target-entity-id-5", properties, then);
+
+        exporter.close();
+
+        File out = new File(pathname);
+        assertTrue(pathname + " exists", out.exists());
+
+        BufferedReader reader = new BufferedReader(new FileReader(pathname));
+
+        GsonBuilder gsonBuilder = new GsonBuilder();
+        gsonBuilder.registerTypeAdapter(DateTime.class, new DateTimeAdapter());
+        Gson gson = gsonBuilder.create();
+
+        String json1 = reader.readLine();
+        Event event1 = gson.fromJson(json1, Event.class);
+        assertEquals("event-1", event1.getEvent());
+        assertEquals("entity-type-1", event1.getEntityType());
+        assertEquals("entity-id-1", event1.getEntityId());
+        assertNull(event1.getTargetEntityType());
+        assertNull(event1.getTargetEntityId());
+        assertTrue(event1.getProperties().isEmpty());
+        assertEquals(new DateTime().getMillis(), event1.getEventTime().getMillis(), 1000);
+
+        String json2 = reader.readLine();
+        Event event2 = gson.fromJson(json2, Event.class);
+        assertEquals("event-2", event2.getEvent());
+        assertEquals("entity-type-2", event2.getEntityType());
+        assertEquals("entity-id-2", event2.getEntityId());
+        assertEquals("target-entity-type-2", event2.getTargetEntityType());
+        assertNull(event2.getTargetEntityId());
+        assertTrue(event2.getProperties().isEmpty());
+        assertEquals(new DateTime().getMillis(), event2.getEventTime().getMillis(), 1000);
+
+        String json3 = reader.readLine();
+        Event event3 = gson.fromJson(json3, Event.class);
+        assertEquals("event-3", event3.getEvent());
+        assertEquals("entity-type-3", event3.getEntityType());
+        assertEquals("entity-id-3", event3.getEntityId());
+        assertNull(event3.getTargetEntityType());
+        assertEquals("target-entity-id-3", event3.getTargetEntityId());
+        assertTrue(event3.getProperties().isEmpty());
+        assertEquals(new DateTime().getMillis(), event3.getEventTime().getMillis(), 1000);
+
+        String json4 = reader.readLine();
+        Event event4 = gson.fromJson(json4, Event.class);
+        assertEquals("event-4", event4.getEvent());
+        assertEquals("entity-type-4", event4.getEntityType());
+        assertEquals("entity-id-4", event4.getEntityId());
+        assertNull(event4.getTargetEntityType());
+        assertNull(event4.getTargetEntityId());
+        assertEquals(1, event4.getProperties().size());
+        assertEquals(properties.get("birthday"), new DateTime(event4.getProperties().get("birthday")));
+        assertEquals(then.getMillis(), event4.getEventTime().getMillis());
+
+        String json5 = reader.readLine();
+        Event event5 = gson.fromJson(json5, Event.class);
+        assertEquals("event-5", event5.getEvent());
+        assertEquals("entity-type-5", event5.getEntityType());
+        assertEquals("entity-id-5", event5.getEntityId());
+        assertEquals("target-entity-type-5", event5.getTargetEntityType());
+        assertEquals("target-entity-id-5", event5.getTargetEntityId());
+        assertEquals(1, event5.getProperties().size());
+        assertEquals(properties.get("birthday"), new DateTime(event5.getProperties().get("birthday")));
+        assertEquals(then.getMillis(), event4.getEventTime().getMillis());
+
+        String empty = reader.readLine();
+        assertNull("no more data", empty);
+    }
+}
diff --git a/examples/import/pom.xml b/examples/import/pom.xml
index 9f6bd49..fef7ec1 100644
--- a/examples/import/pom.xml
+++ b/examples/import/pom.xml
@@ -3,7 +3,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>io.prediction.samples</groupId>
     <artifactId>sample-import</artifactId>
-    <version>0.8.3</version>
+    <version>0.9.5</version>
     <packaging>jar</packaging>
     <name>PredictionIO Java SDK Examples: Import</name>
 
@@ -11,7 +11,7 @@
         <dependency>
             <groupId>io.prediction</groupId>
             <artifactId>client</artifactId>
-            <version>0.8.3</version>
+            <version>0.9.5</version>
         </dependency>
     </dependencies>
 
diff --git a/examples/quickstart_import/pom.xml b/examples/quickstart_import/pom.xml
index e7f0755..67595f6 100644
--- a/examples/quickstart_import/pom.xml
+++ b/examples/quickstart_import/pom.xml
@@ -3,7 +3,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>io.prediction.samples</groupId>
     <artifactId>quickstart-import</artifactId>
-    <version>0.8.3</version>
+    <version>0.9.5</version>
     <packaging>jar</packaging>
     <name>PredictionIO Java SDK Examples: Quickstart Import</name>
 
@@ -11,7 +11,7 @@
         <dependency>
             <groupId>io.prediction</groupId>
             <artifactId>client</artifactId>
-            <version>0.8.3</version>
+            <version>0.9.5</version>
         </dependency>
     </dependencies>
 
diff --git a/examples/quickstart_import/src/main/java/io/prediction/samples/QuickstartImport.java b/examples/quickstart_import/src/main/java/io/prediction/samples/QuickstartImport.java
index c82fb3f..257e0ae 100644
--- a/examples/quickstart_import/src/main/java/io/prediction/samples/QuickstartImport.java
+++ b/examples/quickstart_import/src/main/java/io/prediction/samples/QuickstartImport.java
@@ -9,6 +9,12 @@
 import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.ExecutionException;
+import java.util.List;
+import java.util.LinkedList;
+import org.joda.time.DateTime;
+
+import io.prediction.Event;
+
 
 public class QuickstartImport {
     public static void main(String[] args)
@@ -45,6 +51,27 @@
             }
         }
 
+        List<Event> events = new LinkedList<Event>();
+     
+        // Use only 5 users because max batch size is 50
+        // Throws IOException w/ details inside if this is exceeded
+        for (int user = 1; user <= 5; user++) {
+            for (int i = 1; i <= 10; i++) {
+                int item = rand.nextInt(50) + 1;
+                System.out.println("User " + user + " views item " + item);
+                events.add(new Event()
+            .event("view")
+            .entityType("user")
+            .entityId(""+user)
+            .targetEntityType("item")
+            .targetEntityId(""+item)
+            .properties(emptyProperty)
+            .eventTime(new DateTime()));
+            }
+        }
+    
+        client.createEvents(events);
+
         client.close();
     }
 }
diff --git a/pom.xml b/pom.xml
index b4693b6..1500ed8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>io.prediction</groupId>
   <artifactId>sdk</artifactId>
-  <version>0.8.3</version>
+  <version>0.9.6-SNAPSHOT</version>
   <url>http://prediction.io</url>
   <packaging>pom</packaging>
   <name>PredictionIO Java SDK</name>