killbill-memoizeit

Changes

account/pom.xml 2(+1 -1)

api/pom.xml 2(+1 -1)

beatrix/pom.xml 2(+1 -1)

bin/start-server 4(+2 -2)

catalog/pom.xml 2(+1 -1)

invoice/pom.xml 2(+1 -1)

jaxrs/pom.xml 2(+1 -1)

junction/pom.xml 2(+1 -1)

osgi/pom.xml 2(+1 -1)

overdue/pom.xml 2(+1 -1)

payment/pom.xml 2(+1 -1)

pom.xml 11(+8 -3)

server/pom.xml 2(+1 -1)

tenant/pom.xml 2(+1 -1)

usage/pom.xml 2(+1 -1)

util/pom.xml 2(+1 -1)

Details

account/pom.xml 2(+1 -1)

diff --git a/account/pom.xml b/account/pom.xml
index 47320b3..accc6d7 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-account</artifactId>
diff --git a/analytics/pom.xml b/analytics/pom.xml
index dc6d8e4..0f2bb10 100644
--- a/analytics/pom.xml
+++ b/analytics/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-analytics</artifactId>

api/pom.xml 2(+1 -1)

diff --git a/api/pom.xml b/api/pom.xml
index c660478..3c74300 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-api</artifactId>

beatrix/pom.xml 2(+1 -1)

diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index aa0fa4a..35a287e 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-beatrix</artifactId>

bin/start-server 4(+2 -2)

diff --git a/bin/start-server b/bin/start-server
index 2b42e28..f6132f6 100755
--- a/bin/start-server
+++ b/bin/start-server
@@ -66,7 +66,7 @@ function start() {
     mkdir -p $LOG_DIR
 
     local opts=`build_properties`
-    local start_cmd="mvn $opts -Dlogback.configurationFile=$LOG -Dning.jmx.http.port=$PORT -Dxn.host.external.port=$PORT -DjettyPort=$PORT -Dxn.server.port=$PORT jetty:run"
+    local start_cmd="mvn $opts -Dlogback.configurationFile=$LOG jetty:run"
 
     local debug_opts_eclipse=
     if [ ! -z $DEBUG ]; then
@@ -76,7 +76,7 @@ function start() {
             debug_opts_eclipse=$DEBUG_OPTS_ECLIPSE
         fi
     fi
-    export MAVEN_OPTS="$MAVEN_OPTS -Duser.timezone=UTC $debug_opts_eclipse"
+    export MAVEN_OPTS="$MAVEN_OPTS $debug_opts_eclipse"
 
     echo "Starting IRS MAVEN_OPTS = $MAVEN_OPTS"
     echo "$start_cmd"

catalog/pom.xml 2(+1 -1)

diff --git a/catalog/pom.xml b/catalog/pom.xml
index e1f76fa..482fb4c 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-catalog</artifactId>
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index 8e3eedf..f4c3355 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-entitlement</artifactId>

invoice/pom.xml 2(+1 -1)

diff --git a/invoice/pom.xml b/invoice/pom.xml
index bcfce68..9994507 100644
--- a/invoice/pom.xml
+++ b/invoice/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-invoice</artifactId>

jaxrs/pom.xml 2(+1 -1)

diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml
index a394c0f..9dd9a85 100644
--- a/jaxrs/pom.xml
+++ b/jaxrs/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-jaxrs</artifactId>
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PluginResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PluginResource.java
index fae67ff..bd3462c 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PluginResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PluginResource.java
@@ -16,11 +16,16 @@
 
 package com.ning.billing.jaxrs.resources;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
 
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
@@ -35,12 +40,16 @@ import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.core.Response;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import com.ning.billing.jaxrs.util.Context;
 import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
 import com.ning.billing.util.api.AuditUserApi;
 import com.ning.billing.util.api.CustomFieldUserApi;
 import com.ning.billing.util.api.TagUserApi;
 
+import com.google.common.io.ByteStreams;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
@@ -49,6 +58,9 @@ import com.google.inject.name.Named;
 @Path(JaxrsResource.PLUGINS_PATH + "{subResources:.*}")
 public class PluginResource extends JaxRsResourceBase {
 
+    private static final Logger log = LoggerFactory.getLogger(PluginResource.class);
+    private static final Charset UTF_8 = Charset.forName("UTF-8");
+
     private final HttpServlet osgiServlet;
 
     @Inject
@@ -116,9 +128,43 @@ public class PluginResource extends JaxRsResourceBase {
     private Response serviceViaOSGIPlugin(final HttpServletRequest request, final HttpServletResponse response,
                                           final ServletContext servletContext, final ServletConfig servletConfig) throws ServletException, IOException {
         prepareOSGIRequest(request, servletContext, servletConfig);
-        osgiServlet.service(new OSGIServletRequestWrapper(request), new OSGIServletResponseWrapper(response));
+        osgiServlet.service(new OSGIServletRequestWrapper(request, createInputStream(request)), new OSGIServletResponseWrapper(response));
+
+        if (response.isCommitted()) {
+            if (response.getStatus() >= 400) {
+                log.warn("{} responded {}", request.getPathInfo(), response.getStatus());
+            }
+            // Jersey will want to return 204, but the servlet should have done the right thing already
+            return null;
+        } else {
+            return Response.status(response.getStatus()).build();
+        }
+    }
 
-        return Response.status(response.getStatus()).build();
+    private InputStream createInputStream(final HttpServletRequest request) throws IOException {
+        // /!\ Kludge alert (pierre) /!\
+        // This is awful... But because of various servlet filters we have in place, include Shiro,
+        // the request parameters and/or body at this point have already been looked at.
+        // We can't use @FormParam in PluginResource because we don't know the form parameter names
+        // in advance.
+        // So... We just stick them back in :-)
+        // TODO Support x-www-form-urlencoded vs multipart/form-data
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        for (final String key : request.getParameterMap().keySet()) {
+            out.write((key + "=").getBytes(UTF_8));
+
+            int idx = 0;
+            for (final String value : request.getParameterMap().get(key)) {
+                if (idx > 0) {
+                    out.write("&".getBytes(UTF_8));
+                }
+                idx++;
+                out.write(value.getBytes(UTF_8));
+            }
+        }
+        ByteStreams.copy(request.getInputStream(), out);
+
+        return new ByteArrayInputStream(out.toByteArray());
     }
 
     private void prepareOSGIRequest(final HttpServletRequest request, final ServletContext servletContext, final ServletConfig servletConfig) {
@@ -126,11 +172,14 @@ public class PluginResource extends JaxRsResourceBase {
         request.setAttribute("killbill.osgi.servletConfig", servletConfig);
     }
 
-    // Request wrapper to hide the /plugins prefix to OSGI bundles
+    // Request wrapper to hide the /plugins prefix to OSGI bundles and fiddle with the input stream
     private static final class OSGIServletRequestWrapper extends HttpServletRequestWrapper {
 
-        public OSGIServletRequestWrapper(final HttpServletRequest request) {
+        private final InputStream inputStream;
+
+        public OSGIServletRequestWrapper(final HttpServletRequest request, final InputStream inputStream) {
             super(request);
+            this.inputStream = inputStream;
         }
 
         @Override
@@ -147,6 +196,25 @@ public class PluginResource extends JaxRsResourceBase {
         public String getServletPath() {
             return super.getServletPath().replace(JaxrsResource.PLUGINS_PATH, "");
         }
+
+        @Override
+        public ServletInputStream getInputStream() throws IOException {
+            return new ServletInputStreamWrapper(inputStream);
+        }
+    }
+
+    private static final class ServletInputStreamWrapper extends ServletInputStream {
+
+        private final InputStream inputStream;
+
+        public ServletInputStreamWrapper(final InputStream inputStream) {
+            this.inputStream = inputStream;
+        }
+
+        @Override
+        public int read() throws IOException {
+            return inputStream.read();
+        }
     }
 
     private static final class OSGIServletResponseWrapper extends HttpServletResponseWrapper {

junction/pom.xml 2(+1 -1)

diff --git a/junction/pom.xml b/junction/pom.xml
index a3244fa..c5b58db 100644
--- a/junction/pom.xml
+++ b/junction/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-junction</artifactId>

osgi/pom.xml 2(+1 -1)

diff --git a/osgi/pom.xml b/osgi/pom.xml
index 3ffc07c..c6e0bc6 100644
--- a/osgi/pom.xml
+++ b/osgi/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi</artifactId>
diff --git a/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIKillbill.java b/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIKillbill.java
index 18014ed..c24f217 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIKillbill.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIKillbill.java
@@ -17,8 +17,6 @@
 package com.ning.billing.osgi;
 
 import javax.inject.Inject;
-import javax.inject.Named;
-import javax.sql.DataSource;
 
 import com.ning.billing.account.api.AccountUserApi;
 import com.ning.billing.analytics.api.sanity.AnalyticsSanityApi;
@@ -34,7 +32,6 @@ import com.ning.billing.invoice.api.InvoiceUserApi;
 import com.ning.billing.junction.api.JunctionApi;
 import com.ning.billing.osgi.api.OSGIKillbill;
 import com.ning.billing.osgi.api.config.PluginConfigServiceApi;
-import com.ning.billing.osgi.glue.DefaultOSGIModule;
 import com.ning.billing.overdue.OverdueUserApi;
 import com.ning.billing.payment.api.PaymentApi;
 import com.ning.billing.tenant.api.TenantUserApi;
diff --git a/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java b/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java
index cc122c0..fe138d4 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java
@@ -22,9 +22,6 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.jar.Attributes;
diff --git a/osgi/src/main/java/com/ning/billing/osgi/KillbillEventObservable.java b/osgi/src/main/java/com/ning/billing/osgi/KillbillEventObservable.java
index 14d7f4d..c25cb16 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/KillbillEventObservable.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/KillbillEventObservable.java
@@ -20,7 +20,6 @@ import java.util.Observable;
 
 import javax.inject.Inject;
 
-import org.osgi.framework.BundleContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/osgi-bundles/bundles/analytics/pom.xml b/osgi-bundles/bundles/analytics/pom.xml
index 55bc399..ceeee99 100644
--- a/osgi-bundles/bundles/analytics/pom.xml
+++ b/osgi-bundles/bundles/analytics/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-bundles-analytics</artifactId>
@@ -56,6 +56,10 @@
         </dependency>
         <dependency>
             <groupId>com.ning.billing.commons</groupId>
+            <artifactId>killbill-concurrent</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.ning.billing.commons</groupId>
             <artifactId>killbill-embeddeddb</artifactId>
         </dependency>
         <dependency>
@@ -81,6 +85,11 @@
             <scope>runtime</scope>
         </dependency>
         <dependency>
+            <groupId>org.ini4j</groupId>
+            <artifactId>ini4j</artifactId>
+            <version>0.5.2</version>
+        </dependency>
+        <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.core</artifactId>
         </dependency>
diff --git a/osgi-bundles/bundles/analytics/README.md b/osgi-bundles/bundles/analytics/README.md
new file mode 100644
index 0000000..41847d6
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/README.md
@@ -0,0 +1,47 @@
+Analytics plugin
+================
+
+The Analytics plugin provides simple, yet powerful, dashboarding capabilities.
+
+To create a dashboard, go to (http://127.0.0.1:8080/plugins/killbill-analytics/static/analytics.html).
+
+A dashboard is constituted of a number of reports, each of them being numbered, starting from 1. All reports are displayed in a single column, the report number 1 being the top most one. All reports share the same X axis.
+
+Each report can contain one or multiple time series, the source data being a table or a view with the following format:
+
+<table>
+  <tr>
+    <th>SQL column name</th><th>Description</th><th></th>
+  </tr>
+  <tr>
+    <td>pivot</td><td>Subcategory in your data</td><td>Optional</td>
+  </tr>
+  <tr>
+    <td>day</td><td>X values (date or datetime)</td><td>Required</td>
+  </tr>
+  <tr>
+    <td>count</td><td>Y values (float)</td><td>Required</td>
+  </tr>
+</table>
+
+To configure a report, create a INI file in the following format:
+
+    [report_name]
+    tableName = view_or_table_name_to_query
+    prettyName = Pretty name to use for the dashboard legend
+
+The path to the INI file can be configured via -Dcom.ning.billing.osgi.bundles.analytics.reports.configuration
+
+API
+---
+
+The dashboard system is controlled by query parameters:
+
+* **report1**, **report2**, etc.: report name (from the configuration). The number determines in which slot the data should be displayed, starting from the top of the page. For example, report1=trials&report1=conversions&report1=cancellations&report2=accounts will graph the trials, conversions and cancellations reports in the first slot (on the same graph), and the accounts report below (in slot 2)
+* **startDate** and **endDate**: dates to filter the data on the server side. For example: startDate=2012-08-01&endDate=2013-10-01
+* **smooth1**, **smooth2**, etc.: smoothing function to apply for data in a given slot. Currently support smoothing functions are:
+ * AVERAGE\_WEEKLY: average the values on a weekly basis
+ * AVERAGE\_MONTHLY: average the values on a monthly basis
+ * SUM\_WEEKLY: sum all values on a weekly basis
+ * SUM\_MONTHLY: sum all values on a monthly basis
+* To filter pivots from a report, use *!* for exclusions and *$* for inclusions. For example, report1=payments_per_day$AUD$EUR will graph the payments for AUD and EUR only, whereas report1=payments_per_day!AUD!EUR will graph all payments but the ones in AUD and EUR.
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsActivator.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsActivator.java
index f3879c2..e9530c3 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsActivator.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsActivator.java
@@ -27,6 +27,9 @@ import org.osgi.framework.BundleContext;
 import com.ning.billing.osgi.api.OSGIPluginProperties;
 import com.ning.billing.osgi.bundles.analytics.api.user.AnalyticsUserApi;
 import com.ning.billing.osgi.bundles.analytics.http.AnalyticsServlet;
+import com.ning.billing.osgi.bundles.analytics.reports.ReportsConfiguration;
+import com.ning.billing.osgi.bundles.analytics.reports.ReportsUserApi;
+import com.ning.billing.osgi.bundles.analytics.reports.scheduler.JobsScheduler;
 import com.ning.killbill.osgi.libs.killbill.KillbillActivatorBase;
 import com.ning.killbill.osgi.libs.killbill.OSGIKillbillEventDispatcher.OSGIKillbillEventHandler;
 
@@ -35,22 +38,40 @@ public class AnalyticsActivator extends KillbillActivatorBase {
     public static final String PLUGIN_NAME = "killbill-analytics";
 
     private OSGIKillbillEventHandler analyticsListener;
+    private JobsScheduler jobsScheduler;
+    private ReportsUserApi reportsUserApi;
 
     @Override
     public void start(final BundleContext context) throws Exception {
         super.start(context);
 
-        final Executor executor = BusinessExecutor.create(logService);
+        final Executor executor = BusinessExecutor.newCachedThreadPool();
 
         analyticsListener = new AnalyticsListener(logService, killbillAPI, dataSource, executor);
         dispatcher.registerEventHandler(analyticsListener);
 
+        jobsScheduler = new JobsScheduler(logService, dataSource);
+        final ReportsConfiguration reportsConfiguration = new ReportsConfiguration(logService, jobsScheduler);
+        reportsConfiguration.initialize();
+
         final AnalyticsUserApi analyticsUserApi = new AnalyticsUserApi(logService, killbillAPI, dataSource, executor);
-        final AnalyticsServlet analyticsServlet = new AnalyticsServlet(analyticsUserApi, logService);
+        reportsUserApi = new ReportsUserApi(dataSource, reportsConfiguration);
+        final AnalyticsServlet analyticsServlet = new AnalyticsServlet(analyticsUserApi, reportsUserApi, logService);
         registerServlet(context, analyticsServlet);
     }
 
     @Override
+    public void stop(final BundleContext context) throws Exception {
+        if (jobsScheduler != null) {
+            jobsScheduler.shutdownNow();
+        }
+        if (reportsUserApi != null) {
+            reportsUserApi.shutdownNow();
+        }
+        super.stop(context);
+    }
+
+    @Override
     public OSGIKillbillEventHandler getOSGIKillbillEventHandler() {
         return analyticsListener;
     }
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/BusinessExecutor.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/BusinessExecutor.java
index a496f78..995ee80 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/BusinessExecutor.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/BusinessExecutor.java
@@ -16,155 +16,37 @@
 
 package com.ning.billing.osgi.bundles.analytics;
 
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
-import java.util.concurrent.SynchronousQueue;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
 
-import org.osgi.service.log.LogService;
-
-import com.ning.killbill.osgi.libs.killbill.OSGIKillbillLogService;
+import com.ning.billing.commons.concurrent.Executors;
 
 import com.google.common.annotations.VisibleForTesting;
 
-public class BusinessExecutor extends ThreadPoolExecutor {
+public class BusinessExecutor {
 
     @VisibleForTesting
-    static final Integer NB_THREADS = Integer.valueOf(System.getProperty("com.ning.billing.osgi.bundles.analytics.nb_threads", "100"));
-
-    private final OSGIKillbillLogService logService;
+    static final Integer NB_THREADS = Integer.valueOf(System.getProperty("com.ning.billing.osgi.bundles.analytics.refresh.nb_threads", "100"));
 
-    public static BusinessExecutor create(final OSGIKillbillLogService logService) {
-        return new BusinessExecutor(0,
-                                    NB_THREADS,
-                                    60L,
-                                    TimeUnit.SECONDS,
-                                    new SynchronousQueue<Runnable>(),
-                                    new NamedThreadFactory("osgi-analytics"),
-                                    logService);
+    public static ExecutorService newCachedThreadPool() {
+        return newCachedThreadPool(NB_THREADS, "osgi-analytics-refresh");
     }
 
-    public BusinessExecutor(final int corePoolSize,
-                            final int maximumPoolSize,
-                            final long keepAliveTime,
-                            final TimeUnit unit,
-                            final BlockingQueue<Runnable> workQueue,
-                            final ThreadFactory threadFactory,
-                            final OSGIKillbillLogService logService) {
+    public static ExecutorService newCachedThreadPool(final int nbThreads, final String name) {
         // Note: we don't use the default rejection handler here (AbortPolicy) as we always want the tasks to be executed
-        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, new CallerRunsPolicy());
-        this.logService = logService;
-    }
-
-    @Override
-    public <T> Future<T> submit(final Callable<T> task) {
-        return super.submit(WrappedCallable.wrap(logService, task));
-    }
-
-    @Override
-    public <T> Future<T> submit(final Runnable task, final T result) {
-        // HACK: assumes ThreadPoolExecutor will create a callable and call execute()
-        // (can't wrap the runnable here or exception isn't re-thrown when Future.get() is called)
-        return super.submit(task, result);
-    }
-
-    @Override
-    public Future<?> submit(final Runnable task) {
-        return super.submit(WrappedRunnable.wrap(logService, task));
-    }
+        return Executors.newCachedThreadPool(0,
+                                             nbThreads,
+                                             name,
+                                             60L,
+                                             TimeUnit.SECONDS,
+                                             new CallerRunsPolicy());
 
-    @Override
-    public void execute(final Runnable command) {
-        super.execute(WrappedRunnable.wrap(logService, command));
     }
 
-    private static class WrappedCallable<T> implements Callable<T> {
-
-        private final OSGIKillbillLogService logService;
-        private final Callable<T> callable;
-
-        private WrappedCallable(final OSGIKillbillLogService logService, final Callable<T> callable) {
-            this.logService = logService;
-            this.callable = callable;
-        }
-
-        public static <T> Callable<T> wrap(final OSGIKillbillLogService logService, final Callable<T> callable) {
-            return callable instanceof WrappedCallable ? callable : new WrappedCallable<T>(logService, callable);
-        }
-
-        @Override
-        public T call() throws Exception {
-            final Thread currentThread = Thread.currentThread();
-
-            try {
-                return callable.call();
-            } catch (Exception e) {
-                // since callables are expected to sometimes throw exceptions, log this at DEBUG instead of ERROR
-                logService.log(LogService.LOG_DEBUG, currentThread + " ended with an exception", e);
-
-                throw e;
-            } catch (Error e) {
-                logService.log(LogService.LOG_ERROR, currentThread + " ended with an exception", e);
-
-                throw e;
-            } finally {
-                logService.log(LogService.LOG_DEBUG, currentThread + " finished executing");
-            }
-        }
-    }
-
-    private static class WrappedRunnable implements Runnable {
-
-        private final OSGIKillbillLogService logService;
-        private final Runnable runnable;
-
-        private WrappedRunnable(final OSGIKillbillLogService logService, final Runnable runnable) {
-            this.logService = logService;
-            this.runnable = runnable;
-        }
-
-        public static Runnable wrap(final OSGIKillbillLogService logService, final Runnable runnable) {
-            return runnable instanceof WrappedRunnable ? runnable : new WrappedRunnable(logService, runnable);
-        }
-
-        @Override
-        public void run() {
-            final Thread currentThread = Thread.currentThread();
-
-            try {
-                runnable.run();
-            } catch (Throwable e) {
-                logService.log(LogService.LOG_ERROR, currentThread + " ended abnormally with an exception", e);
-            }
-
-            logService.log(LogService.LOG_DEBUG, currentThread + " finished executing");
-        }
-    }
-
-    /**
-     * Factory that sets the name of each thread it creates to {@code [name]-[id]}.
-     * This makes debugging stack traces much easier.
-     */
-    private static class NamedThreadFactory implements ThreadFactory {
-
-        private final AtomicInteger count = new AtomicInteger(0);
-        private final String name;
-
-        public NamedThreadFactory(final String name) {
-            this.name = name;
-        }
-
-        @Override
-        public Thread newThread(final Runnable runnable) {
-            final Thread thread = new Thread(runnable);
-
-            thread.setName(name + "-" + count.incrementAndGet());
-
-            return thread;
-        }
+    public static ScheduledExecutorService newSingleThreadScheduledExecutor(final String name) {
+        return Executors.newSingleThreadScheduledExecutor(name,
+                                                          new CallerRunsPolicy());
     }
 }
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java
index 7f72951..9947d93 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java
@@ -38,12 +38,16 @@ import com.ning.billing.osgi.bundles.analytics.AnalyticsRefreshException;
 import com.ning.billing.osgi.bundles.analytics.api.BusinessSnapshot;
 import com.ning.billing.osgi.bundles.analytics.api.user.AnalyticsUserApi;
 import com.ning.billing.osgi.bundles.analytics.json.NamedXYTimeSeries;
+import com.ning.billing.osgi.bundles.analytics.reports.ReportsUserApi;
+import com.ning.billing.osgi.bundles.analytics.reports.analysis.Smoother;
+import com.ning.billing.osgi.bundles.analytics.reports.analysis.Smoother.SmootherType;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.CallOrigin;
 import com.ning.billing.util.callcontext.UserType;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Objects;
+import com.google.common.base.Strings;
 import com.google.common.io.Resources;
 
 public class AnalyticsServlet extends HttpServlet {
@@ -65,13 +69,19 @@ public class AnalyticsServlet extends HttpServlet {
     private final static String QUERY_END_DATE = "endDate";
     private final static String QUERY_PRODUCTS = "products";
 
+    private static final String REPORTS = "reports";
+    private static final String REPORTS_QUERY_NAME = "name";
+    private static final String REPORTS_SMOOTHER_NAME = "smooth";
+
     private static final ObjectMapper mapper = ObjectMapperProvider.get();
 
     private final AnalyticsUserApi analyticsUserApi;
+    private final ReportsUserApi reportsUserApi;
     private final LogService logService;
 
-    public AnalyticsServlet(final AnalyticsUserApi analyticsUserApi, final LogService logService) {
+    public AnalyticsServlet(final AnalyticsUserApi analyticsUserApi, final ReportsUserApi reportsUserApi, final LogService logService) {
         this.analyticsUserApi = analyticsUserApi;
+        this.reportsUserApi = reportsUserApi;
         this.logService = logService;
     }
 
@@ -111,6 +121,8 @@ public class AnalyticsServlet extends HttpServlet {
 
         } else if (uriOperationInfo.startsWith(STATIC_RESOURCES)) {
             doHandleStaticResource(uriOperationInfo, resp);
+        } else if (uriOperationInfo.startsWith(REPORTS)) {
+            doHandleReports(req, resp);
         } else {
             final UUID kbAccountId = getKbAccountId(req, resp);
             final CallContext context = createCallContext(req, resp);
@@ -160,6 +172,26 @@ public class AnalyticsServlet extends HttpServlet {
         return res;
     }
 
+    private void doHandleReports(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+        final String[] rawReportNames = req.getParameterValues(REPORTS_QUERY_NAME);
+        if (rawReportNames == null || rawReportNames.length == 0) {
+            resp.sendError(404);
+            return;
+        }
+
+        final LocalDate startDate = Strings.emptyToNull(req.getParameter(QUERY_START_DATE)) != null ? DATE_FORMAT.parseLocalDate(req.getParameter(QUERY_START_DATE)) : null;
+        final LocalDate endDate = Strings.emptyToNull(req.getParameter(QUERY_END_DATE)) != null ? DATE_FORMAT.parseLocalDate(req.getParameter(QUERY_END_DATE)) : null;
+
+        final SmootherType smootherType = Smoother.fromString(Strings.emptyToNull(req.getParameter(REPORTS_SMOOTHER_NAME)));
+
+        // TODO PIERRE Switch to an equivalent of StreamingOutputStream?
+        final List<NamedXYTimeSeries> result = reportsUserApi.getTimeSeriesDataForReport(rawReportNames, startDate, endDate, smootherType);
+
+        resp.getOutputStream().write(mapper.writeValueAsBytes(result));
+        resp.setContentType("application/json");
+        setCrossSiteScriptingHeaders(resp);
+        resp.setStatus(HttpServletResponse.SC_OK);
+    }
 
     private void doHandlePlanTransitionsOverTime(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
 
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/json/XY.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/json/XY.java
index 7ae8f20..bd5ed23 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/json/XY.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/json/XY.java
@@ -16,25 +16,52 @@
 
 package com.ning.billing.osgi.bundles.analytics.json;
 
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+
 import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 public class XY {
 
     private final String x;
-    private final Integer y;
+    private final Float y;
+
+    private final DateTime xDate;
 
     @JsonCreator
-    public XY(@JsonProperty("x") final String x, @JsonProperty("y") final Integer y) {
+    public XY(@JsonProperty("x") final String x, @JsonProperty("y") final Float y) {
         this.x = x;
         this.y = y;
+        this.xDate = new DateTime(x, DateTimeZone.UTC);
+    }
+
+    public XY(final String x, final Integer y) {
+        this(x, new Float(y.doubleValue()));
+    }
+
+    public XY(final DateTime xDate, final Float y) {
+        this.x = xDate.toString();
+        this.y = y;
+        this.xDate = xDate;
+    }
+
+    public XY(final LocalDate xDate, final Float y) {
+        this(xDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), y);
     }
 
     public String getX() {
         return x;
     }
 
-    public Integer getY() {
+    @JsonIgnore
+    public DateTime getxDate() {
+        return xDate;
+    }
+
+    public Float getY() {
         return y;
     }
 }
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/AverageSmoother.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/AverageSmoother.java
new file mode 100644
index 0000000..aaff67b
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/AverageSmoother.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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.ning.billing.osgi.bundles.analytics.reports.analysis;
+
+import java.util.List;
+import java.util.Map;
+
+import com.ning.billing.osgi.bundles.analytics.json.XY;
+
+public class AverageSmoother extends Smoother {
+
+    public AverageSmoother(final Map<String, Map<String, List<XY>>> dataForReports, final DateGranularity dateGranularity) {
+        super(dataForReports, dateGranularity);
+    }
+
+    @Override
+    public float computeSmoothedValue(final float accumulator, final int accumulatorSize) {
+        return accumulator / accumulatorSize;
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/DateGranularity.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/DateGranularity.java
new file mode 100644
index 0000000..89579e8
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/DateGranularity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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.ning.billing.osgi.bundles.analytics.reports.analysis;
+
+public enum DateGranularity {
+    WEEKLY,
+    MONTHLY
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/Smoother.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/Smoother.java
new file mode 100644
index 0000000..6ce8a8e
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/Smoother.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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.ning.billing.osgi.bundles.analytics.reports.analysis;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTimeConstants;
+import org.joda.time.LocalDate;
+
+import com.ning.billing.osgi.bundles.analytics.json.XY;
+
+import com.google.common.base.Function;
+
+public abstract class Smoother {
+
+    private final Map<String, Map<String, List<XY>>> dataForReports;
+    private final DateGranularity dateGranularity;
+
+    public static enum SmootherType {
+        AVERAGE_WEEKLY,
+        AVERAGE_MONTHLY,
+        SUM_WEEKLY,
+        SUM_MONTHLY;
+
+        public Smoother createSmoother(final Map<String, Map<String, List<XY>>> dataForReports) {
+            switch (this) {
+                case AVERAGE_WEEKLY:
+                    return new AverageSmoother(dataForReports, DateGranularity.WEEKLY);
+                case AVERAGE_MONTHLY:
+                    return new AverageSmoother(dataForReports, DateGranularity.MONTHLY);
+                case SUM_WEEKLY:
+                    return new SummingSmoother(dataForReports, DateGranularity.WEEKLY);
+                case SUM_MONTHLY:
+                    return new SummingSmoother(dataForReports, DateGranularity.MONTHLY);
+                default:
+                    return null;
+            }
+        }
+    }
+
+    public static SmootherType fromString(@Nullable final String smootherName) {
+        if (smootherName == null) {
+            return null;
+        } else {
+            return SmootherType.valueOf(smootherName.toUpperCase());
+        }
+    }
+
+    public Smoother(final Map<String, Map<String, List<XY>>> dataForReports, final DateGranularity dateGranularity) {
+        this.dataForReports = dataForReports;
+        this.dateGranularity = dateGranularity;
+    }
+
+    public abstract float computeSmoothedValue(float accumulator, int accumulatorSize);
+
+    // Assume the data is already sorted
+    public void smooth() {
+        for (final Map<String, List<XY>> dataForReport : dataForReports.values()) {
+            for (final String pivotName : dataForReport.keySet()) {
+                final List<XY> dataForPivot = dataForReport.get(pivotName);
+                final List<XY> smoothedData = smooth(dataForPivot);
+                dataForReport.put(pivotName, smoothedData);
+            }
+        }
+    }
+
+    public Map<String, Map<String, List<XY>>> getDataForReports() {
+        return dataForReports;
+    }
+
+    private List<XY> smooth(final List<XY> inputData) {
+        switch (dateGranularity) {
+            case WEEKLY:
+                return smooth(inputData,
+                              new Function<XY, LocalDate>() {
+                                  @Override
+                                  public LocalDate apply(final XY input) {
+                                      return input.getxDate().toLocalDate().withDayOfWeek(DateTimeConstants.MONDAY);
+                                  }
+                              });
+            case MONTHLY:
+                return smooth(inputData,
+                              new Function<XY, LocalDate>() {
+                                  @Override
+                                  public LocalDate apply(final XY input) {
+                                      return input.getxDate().toLocalDate().withDayOfMonth(1);
+                                  }
+                              });
+            default:
+                return inputData;
+        }
+    }
+
+    private List<XY> smooth(final List<XY> inputData, final Function<XY, LocalDate> truncator) {
+        final List<XY> smoothedData = new LinkedList<XY>();
+
+        LocalDate currentTruncatedDate = truncator.apply(inputData.get(0));
+        Float accumulator = (float) 0;
+        int accumulatorSize = 0;
+        for (final XY xy : inputData) {
+            final LocalDate zeTruncatedDate = truncator.apply(xy);
+            //noinspection ConstantConditions
+            if (zeTruncatedDate.compareTo(currentTruncatedDate) != 0) {
+                smoothedData.add(new XY(currentTruncatedDate, computeSmoothedValue(accumulator, accumulatorSize)));
+                accumulator = (float) 0;
+                accumulatorSize = 0;
+            }
+
+            accumulator += xy.getY();
+            accumulatorSize++;
+            currentTruncatedDate = zeTruncatedDate;
+        }
+
+        return smoothedData;
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/SummingSmoother.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/SummingSmoother.java
new file mode 100644
index 0000000..890979c
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/SummingSmoother.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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.ning.billing.osgi.bundles.analytics.reports.analysis;
+
+import java.util.List;
+import java.util.Map;
+
+import com.ning.billing.osgi.bundles.analytics.json.XY;
+
+public class SummingSmoother extends Smoother {
+
+    public SummingSmoother(final Map<String, Map<String, List<XY>>> dataForReports, final DateGranularity dateGranularity) {
+        super(dataForReports, dateGranularity);
+    }
+
+    @Override
+    public float computeSmoothedValue(final float accumulator, final int accumulatorSize) {
+        return accumulator;
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportConfigurationSection.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportConfigurationSection.java
new file mode 100644
index 0000000..eb2a791
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportConfigurationSection.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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.ning.billing.osgi.bundles.analytics.reports;
+
+public interface ReportConfigurationSection {
+
+    public static enum Frequency {
+        HOURLY,
+        DAILY
+    }
+
+    public String getStoredProcedureName();
+
+    public Frequency getFrequency();
+
+    // For DAILY only
+    public Integer getRefreshTimeOfTheDayGMT();
+
+    public String getTableName();
+
+    public String getPrettyName();
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsConfiguration.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsConfiguration.java
new file mode 100644
index 0000000..4cabf7f
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsConfiguration.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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.ning.billing.osgi.bundles.analytics.reports;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.ini4j.Ini;
+import org.ini4j.Profile.Section;
+import org.osgi.service.log.LogService;
+
+import com.ning.billing.osgi.bundles.analytics.reports.scheduler.JobsScheduler;
+import com.ning.killbill.osgi.libs.killbill.OSGIKillbillLogService;
+
+import com.google.common.base.Objects;
+
+public class ReportsConfiguration {
+
+    private static final String REPORTS_CONFIGURATION_FILE_PATH = System.getProperty("com.ning.billing.osgi.bundles.analytics.reports.configuration");
+
+    private final Map<String, ReportConfigurationSection> configurationPerReport = new LinkedHashMap<String, ReportConfigurationSection>();
+
+    private final OSGIKillbillLogService logService;
+    private final JobsScheduler scheduler;
+
+    public ReportsConfiguration(final OSGIKillbillLogService logService, final JobsScheduler scheduler) {
+        this.logService = logService;
+        this.scheduler = scheduler;
+    }
+
+    public void initialize() {
+        try {
+            parseConfigurationFile();
+        } catch (IOException e) {
+            logService.log(LogService.LOG_WARNING, "Error during initialization", e);
+        }
+    }
+
+    public String getTableNameForReport(final String reportName) {
+        if (configurationPerReport.get(reportName) != null) {
+            return configurationPerReport.get(reportName).getTableName();
+        } else {
+            return null;
+        }
+    }
+
+    public String getPrettyNameForReport(final String reportName) {
+        if (configurationPerReport.get(reportName) != null) {
+            return Objects.firstNonNull(configurationPerReport.get(reportName).getPrettyName(), reportName);
+        } else {
+            return reportName;
+        }
+    }
+
+    private void parseConfigurationFile() throws IOException {
+        if (REPORTS_CONFIGURATION_FILE_PATH == null) {
+            return;
+        }
+
+        final File configurationFile = new File(REPORTS_CONFIGURATION_FILE_PATH);
+        //noinspection MismatchedQueryAndUpdateOfCollection
+        final Ini ini = new Ini(configurationFile);
+        for (final String reportName : ini.keySet()) {
+            final Section section = ini.get(reportName);
+            Thread.currentThread().setContextClassLoader(ReportsConfiguration.class.getClassLoader());
+            final ReportConfigurationSection reportConfigurationSection = section.as(ReportConfigurationSection.class);
+
+            if (reportConfigurationSection.getFrequency() != null && reportConfigurationSection.getStoredProcedureName() != null) {
+                scheduler.schedule(reportName,
+                                   reportConfigurationSection.getStoredProcedureName(),
+                                   reportConfigurationSection.getFrequency(),
+                                   reportConfigurationSection.getRefreshTimeOfTheDayGMT());
+            }
+
+            configurationPerReport.put(reportName, reportConfigurationSection);
+        }
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportSpecification.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportSpecification.java
new file mode 100644
index 0000000..a569ab8
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportSpecification.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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.ning.billing.osgi.bundles.analytics.reports;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import com.google.common.base.Splitter;
+
+public class ReportSpecification {
+
+    private final List<String> pivotNamesToExclude = new ArrayList<String>();
+    private final List<String> pivotNamesToInclude = new ArrayList<String>();
+
+    private final String rawReportName;
+
+    private String reportName;
+
+    public ReportSpecification(final String rawReportName) {
+        this.rawReportName = rawReportName;
+        parseRawReportName();
+    }
+
+    public List<String> getPivotNamesToExclude() {
+        return pivotNamesToExclude;
+    }
+
+    public List<String> getPivotNamesToInclude() {
+        return pivotNamesToInclude;
+    }
+
+    public String getReportName() {
+        return reportName;
+    }
+
+    private void parseRawReportName() {
+        final boolean hasExcludes = rawReportName.contains("!");
+        final boolean hasIncludes = rawReportName.contains("$");
+        if (hasExcludes && hasIncludes) {
+            throw new IllegalArgumentException();
+        }
+
+        // rawReportName is in the form payments_per_day!AUD!BRL or payments_per_day$USD$EUR (but not both!)
+        final Iterator<String> reportIterator = Splitter.on(Pattern.compile("[\\!\\$]"))
+                                                        .trimResults()
+                                                        .omitEmptyStrings()
+                                                        .split(rawReportName)
+                                                        .iterator();
+        boolean isFirst = true;
+        while (reportIterator.hasNext()) {
+            final String piece = reportIterator.next();
+
+            if (isFirst) {
+                reportName = piece;
+            } else {
+                if (hasExcludes) {
+                    pivotNamesToExclude.add(piece);
+                } else if (hasIncludes) {
+                    pivotNamesToInclude.add(piece);
+                } else {
+                    throw new IllegalArgumentException();
+                }
+            }
+
+            isFirst = false;
+        }
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsUserApi.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsUserApi.java
new file mode 100644
index 0000000..1dbd9b3
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsUserApi.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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.ning.billing.osgi.bundles.analytics.reports;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+
+import com.ning.billing.osgi.bundles.analytics.BusinessExecutor;
+import com.ning.billing.osgi.bundles.analytics.dao.BusinessDBIProvider;
+import com.ning.billing.osgi.bundles.analytics.json.NamedXYTimeSeries;
+import com.ning.billing.osgi.bundles.analytics.json.XY;
+import com.ning.billing.osgi.bundles.analytics.reports.analysis.Smoother;
+import com.ning.billing.osgi.bundles.analytics.reports.analysis.Smoother.SmootherType;
+import com.ning.killbill.osgi.libs.killbill.OSGIKillbillDataSource;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
+
+public class ReportsUserApi {
+
+    private static final Integer NB_THREADS = Integer.valueOf(System.getProperty("com.ning.billing.osgi.bundles.analytics.dashboard.nb_threads", "10"));
+    private static final String NO_PIVOT = "____NO_PIVOT____";
+
+    private final ExecutorService dbiThreadsExecutor = BusinessExecutor.newCachedThreadPool(NB_THREADS, "osgi-analytics-dashboard");
+
+    private final IDBI dbi;
+    private final ReportsConfiguration reportsConfiguration;
+
+    public ReportsUserApi(final OSGIKillbillDataSource osgiKillbillDataSource,
+                          final ReportsConfiguration reportsConfiguration) {
+        this.reportsConfiguration = reportsConfiguration;
+        dbi = BusinessDBIProvider.get(osgiKillbillDataSource.getDataSource());
+    }
+
+    public void shutdownNow() {
+        dbiThreadsExecutor.shutdownNow();
+    }
+
+    public List<NamedXYTimeSeries> getTimeSeriesDataForReport(final String[] rawReportNames,
+                                                              @Nullable final LocalDate startDate,
+                                                              @Nullable final LocalDate endDate,
+                                                              @Nullable final SmootherType smootherType) {
+        // Mapping of report name -> pivots -> data
+        final Map<String, Map<String, List<XY>>> dataForReports = new ConcurrentHashMap<String, Map<String, List<XY>>>();
+
+        // Parse the reports
+        final List<ReportSpecification> reportSpecifications = new ArrayList<ReportSpecification>();
+        for (final String rawReportName : rawReportNames) {
+            reportSpecifications.add(new ReportSpecification(rawReportName));
+        }
+
+        // Fetch the data
+        fetchData(reportSpecifications, dataForReports);
+
+        // Filter the data first
+        filterValues(reportSpecifications, dataForReports, startDate, endDate);
+
+        // Normalize and sort the data
+        normalizeAndSortXValues(dataForReports, startDate, endDate);
+
+        // Smooth the data if needed and build the named timeseries
+        if (smootherType != null) {
+            final Smoother smoother = smootherType.createSmoother(dataForReports);
+            smoother.smooth();
+            return buildNamedXYTimeSeries(smoother.getDataForReports());
+        } else {
+            return buildNamedXYTimeSeries(dataForReports);
+        }
+    }
+
+    private List<NamedXYTimeSeries> buildNamedXYTimeSeries(final Map<String, Map<String, List<XY>>> dataForReports) {
+        final List<NamedXYTimeSeries> results = new LinkedList<NamedXYTimeSeries>();
+        for (final String reportName : dataForReports.keySet()) {
+            // Sort the pivots by name for a consistent display in the dashboard
+            for (final String pivotName : Ordering.natural().sortedCopy(dataForReports.get(reportName).keySet())) {
+                final String timeSeriesName;
+                if (NO_PIVOT.equals(pivotName)) {
+                    timeSeriesName = reportsConfiguration.getPrettyNameForReport(reportName);
+                } else {
+                    timeSeriesName = String.format("%s (%s)", reportsConfiguration.getPrettyNameForReport(reportName), pivotName);
+                }
+
+                final List<XY> timeSeries = dataForReports.get(reportName).get(pivotName);
+                results.add(new NamedXYTimeSeries(timeSeriesName, timeSeries));
+            }
+        }
+
+        return results;
+    }
+
+    private void fetchData(final List<ReportSpecification> reportSpecifications, final Map<String, Map<String, List<XY>>> dataForReports) {
+        final List<Future> jobs = new LinkedList<Future>();
+        for (final ReportSpecification reportSpecification : reportSpecifications) {
+            final String reportName = reportSpecification.getReportName();
+            final String tableName = reportsConfiguration.getTableNameForReport(reportName);
+            if (tableName != null) {
+                jobs.add(dbiThreadsExecutor.submit(new Runnable() {
+                    @Override
+                    public void run() {
+                        final Map<String, List<XY>> data = getData(tableName);
+                        dataForReports.put(reportName, data);
+                    }
+                }));
+            }
+        }
+
+        for (final Future job : jobs) {
+            try {
+                job.get();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } catch (ExecutionException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    private void filterValues(final List<ReportSpecification> reportSpecifications,
+                              final Map<String, Map<String, List<XY>>> dataForReports,
+                              @Nullable final LocalDate startDate,
+                              @Nullable final LocalDate endDate) {
+        if (startDate == null && endDate == null) {
+            return;
+        }
+
+        for (final ReportSpecification reportSpecification : reportSpecifications) {
+            final String reportName = reportSpecification.getReportName();
+            final Map<String, List<XY>> dataForReport = dataForReports.get(reportName);
+            if (dataForReport == null) {
+                throw new IllegalArgumentException();
+            }
+
+            // Handle the exclusion list
+            Iterables.removeAll(dataForReport.keySet(), reportSpecification.getPivotNamesToExclude());
+
+            // Handle the inclusion list
+            if (reportSpecification.getPivotNamesToInclude().size() > 0) {
+                Iterables.removeIf(dataForReport.keySet(),
+                                   new Predicate<String>() {
+                                       @Override
+                                       public boolean apply(final String pivotName) {
+                                           return !reportSpecification.getPivotNamesToInclude().contains(pivotName);
+                                       }
+                                   });
+            }
+
+            // Handle the dates filter
+            for (final List<XY> dataForPivot : dataForReport.values()) {
+                Iterables.removeIf(dataForPivot,
+                                   new Predicate<XY>() {
+                                       @Override
+                                       public boolean apply(final XY xy) {
+                                           return startDate != null && xy.getxDate().toLocalDate().isBefore(startDate) ||
+                                                  endDate != null && xy.getxDate().toLocalDate().isAfter(endDate);
+                                       }
+                                   });
+            }
+        }
+    }
+
+    // TODO PIERRE Naive implementation
+    private void normalizeAndSortXValues(final Map<String, Map<String, List<XY>>> dataForReports, @Nullable final LocalDate startDate, @Nullable final LocalDate endDate) {
+        DateTime minDate = null;
+        if (startDate != null) {
+            minDate = startDate.toDateTimeAtStartOfDay(DateTimeZone.UTC);
+        }
+
+        DateTime maxDate = null;
+        if (endDate != null) {
+            maxDate = endDate.toDateTimeAtStartOfDay(DateTimeZone.UTC);
+        }
+
+        // If no min and/or max was specified, infer them from the data
+        if (minDate == null || maxDate == null) {
+            for (final Map<String, List<XY>> dataForReport : dataForReports.values()) {
+                for (final List<XY> dataForPivot : dataForReport.values()) {
+                    for (final XY xy : dataForPivot) {
+                        if (minDate == null || xy.getxDate().isBefore(minDate)) {
+                            minDate = xy.getxDate();
+                        }
+                        if (maxDate == null || xy.getxDate().isAfter(maxDate)) {
+                            maxDate = xy.getxDate();
+                        }
+                    }
+                }
+            }
+        }
+
+        if (minDate == null || maxDate == null) {
+            throw new IllegalStateException();
+        }
+
+        // Add 0 for missing days
+        DateTime curDate = minDate;
+        while (maxDate.isAfter(curDate)) {
+            for (final Map<String, List<XY>> dataForReport : dataForReports.values()) {
+                for (final List<XY> dataForPivot : dataForReport.values()) {
+                    addMissingValueForDateIfNeeded(curDate, dataForPivot);
+                }
+            }
+            curDate = curDate.plusDays(1);
+        }
+
+        // Sort the data for the dashboard
+        for (final String reportName : dataForReports.keySet()) {
+            for (final String pivotName : dataForReports.get(reportName).keySet()) {
+                Collections.sort(dataForReports.get(reportName).get(pivotName),
+                                 new Comparator<XY>() {
+                                     @Override
+                                     public int compare(final XY o1, final XY o2) {
+                                         return o1.getxDate().compareTo(o2.getxDate());
+                                     }
+                                 });
+            }
+        }
+    }
+
+    private void addMissingValueForDateIfNeeded(final DateTime curDate, final List<XY> dataForPivot) {
+        final XY valueForCurrentDate = Iterables.tryFind(dataForPivot, new Predicate<XY>() {
+            @Override
+            public boolean apply(final XY xy) {
+                return xy.getxDate().compareTo(curDate) == 0;
+            }
+        }).orNull();
+
+        if (valueForCurrentDate == null) {
+            dataForPivot.add(new XY(curDate, (float) 0));
+        }
+    }
+
+    private Map<String, List<XY>> getData(final String tableName) {
+        final Map<String, List<XY>> timeSeries = new LinkedHashMap<String, List<XY>>();
+
+        Handle handle = null;
+        try {
+            handle = dbi.open();
+            final List<Map<String, Object>> results = handle.select("select * from " + tableName);
+            for (final Map<String, Object> row : results) {
+                if (row.get("day") == null || row.get("count") == null) {
+                    continue;
+                }
+
+                final String date = row.get("day").toString();
+                final Float value = Float.valueOf(row.get("count").toString());
+
+                if (row.keySet().size() == 2) {
+                    // No pivot
+                    if (timeSeries.get(NO_PIVOT) == null) {
+                        timeSeries.put(NO_PIVOT, new LinkedList<XY>());
+                    }
+                    timeSeries.get(NO_PIVOT).add(new XY(date, value));
+                } else if (row.get("pivot") != null) {
+                    final String pivot = row.get("pivot").toString();
+                    if (timeSeries.get(pivot) == null) {
+                        timeSeries.put(pivot, new LinkedList<XY>());
+                    }
+                    timeSeries.get(pivot).add(new XY(date, value));
+                }
+            }
+        } finally {
+            if (handle != null) {
+                handle.close();
+            }
+        }
+
+        return timeSeries;
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/scheduler/JobsScheduler.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/scheduler/JobsScheduler.java
new file mode 100644
index 0000000..be148fc
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/scheduler/JobsScheduler.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you 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.ning.billing.osgi.bundles.analytics.reports.scheduler;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.osgi.service.log.LogService;
+import org.skife.jdbi.v2.Call;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+
+import com.ning.billing.osgi.bundles.analytics.BusinessExecutor;
+import com.ning.billing.osgi.bundles.analytics.dao.BusinessDBIProvider;
+import com.ning.billing.osgi.bundles.analytics.reports.ReportConfigurationSection.Frequency;
+import com.ning.killbill.osgi.libs.killbill.OSGIKillbillDataSource;
+import com.ning.killbill.osgi.libs.killbill.OSGIKillbillLogService;
+
+public class JobsScheduler {
+
+    private final List<ScheduledExecutorService> jobs = new LinkedList<ScheduledExecutorService>();
+
+    private final OSGIKillbillLogService logService;
+    private final IDBI dbi;
+
+    public JobsScheduler(final OSGIKillbillLogService logService,
+                         final OSGIKillbillDataSource osgiKillbillDataSource) {
+        this.logService = logService;
+        dbi = BusinessDBIProvider.get(osgiKillbillDataSource.getDataSource());
+    }
+
+    public void shutdownNow() {
+        for (final ScheduledExecutorService executor : jobs) {
+            executor.shutdownNow();
+        }
+    }
+
+    public void schedule(final String reportName, final String storedProcedureName, final Frequency frequency, final Integer refreshTimeOfTheDayGMT) {
+        final ScheduledExecutorService executor = BusinessExecutor.newSingleThreadScheduledExecutor("osgi-analytics-reports-" + reportName);
+        jobs.add(executor);
+
+        final StoredProcedureJob command = new StoredProcedureJob(logService, reportName, storedProcedureName, frequency, refreshTimeOfTheDayGMT);
+        executor.scheduleAtFixedRate(command, 0, 1, TimeUnit.MINUTES);
+    }
+
+    private final class StoredProcedureJob implements Runnable {
+
+        private final AtomicLong lastRun = new AtomicLong(0);
+
+        private final LogService logService;
+        private final String reportName;
+        private final String storedProcedureName;
+        private final Frequency frequency;
+        private final Integer refreshTimeOfTheDayGMT;
+
+        private StoredProcedureJob(final LogService logService, final String reportName, final String storedProcedureName,
+                                   final Frequency frequency, final Integer refreshTimeOfTheDayGMT) {
+            this.logService = logService;
+            this.reportName = reportName;
+            this.storedProcedureName = storedProcedureName;
+            this.frequency = frequency;
+            this.refreshTimeOfTheDayGMT = refreshTimeOfTheDayGMT;
+        }
+
+        @Override
+        public void run() {
+            if (!shouldRun()) {
+                return;
+            }
+
+            logService.log(LogService.LOG_INFO, "Starting job for " + reportName);
+
+            callStoredProcedure(storedProcedureName);
+            lastRun.set(System.currentTimeMillis());
+
+            logService.log(LogService.LOG_INFO, "Ending job for " + reportName);
+        }
+
+        private boolean shouldRun() {
+            if (Frequency.HOURLY.equals(frequency) && (System.currentTimeMillis() - lastRun.get()) >= 3600000) {
+                return true;
+            } else if (Frequency.DAILY.equals(frequency)) {
+                if (refreshTimeOfTheDayGMT == null && (System.currentTimeMillis() - lastRun.get()) >= 86400000) {
+                    return true;
+                } else if (refreshTimeOfTheDayGMT != null &&
+                           new DateTime(DateTimeZone.UTC).getHourOfDay() == refreshTimeOfTheDayGMT &&
+                           (System.currentTimeMillis() - lastRun.get()) >= 3600000) {
+                    return true;
+                } else {
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        }
+    }
+
+    private void callStoredProcedure(final String storedProcedureName) {
+        Handle handle = null;
+        try {
+            handle = dbi.open();
+            final Call call = handle.createCall(storedProcedureName);
+            call.invoke();
+        } finally {
+            if (handle != null) {
+                handle.close();
+            }
+        }
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/analytics.ini b/osgi-bundles/bundles/analytics/src/main/resources/reports/analytics.ini
new file mode 100644
index 0000000..4396719
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/analytics.ini
@@ -0,0 +1,89 @@
+;
+; Copyright 2010-2013 Ning, Inc.
+;
+; Ning licenses this file to you 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.
+;
+
+; Built-in default set of reports
+
+[new_trials_last_24_hours]
+tableName = v_new_trials_last_24_hours
+prettyName = Last 24 hours trials
+
+[new_accounts_per_day]
+tableName = v_new_accounts_per_day
+prettyName = Accounts created per day
+
+[new_trials_per_day]
+tableName = v_new_trials_per_day
+prettyName = Trials created per day
+
+[conversions_per_day]
+tableName = v_conversions_per_day
+prettyName = Conversions per day
+
+[cancellations_per_day]
+tableName = v_cancellations_per_day
+prettyName = Effective cancellations per day
+
+[invoices_per_day]
+tableName = v_invoices_per_day
+prettyName = Invoices
+
+[invoice_adjustments_per_day]
+tableName = v_invoice_adjustments_per_day
+prettyName = Invoice adjustments
+
+[invoice_item_adjustments_per_day]
+tableName = v_invoice_item_adjustments_per_day
+prettyName = Invoice item adjustments
+
+[invoice_item_credits_per_day]
+tableName = v_invoice_item_credits_per_day
+prettyName = Invoice item credits
+
+[payments_per_day]
+tableName = v_payments_per_day
+prettyName = Payments
+
+[refunds_per_day]
+tableName = v_refunds_per_day
+prettyName = Refunds
+
+[chargebacks_per_day]
+tableName = v_chargebacks_per_day
+prettyName = Chargebacks
+
+[system_report_notifications_per_queue_name]
+tableName = v_system_report_notifications_per_queue_name
+prettyName = AVAILABLE notifications
+
+[system_report_payment_plugin_failure_aborted]
+tableName = v_system_report_payment_plugin_failure_aborted
+prettyName = PLUGIN_FAILURE_ABORTED and UNKNOWN
+
+[system_report_payment_plugin_failure]
+tableName = v_system_report_payment_plugin_failure
+prettyName = PLUGIN_FAILURE
+
+[system_report_payment_payment_failure_aborted]
+tableName = v_system_report_payment_payment_failure_aborted
+prettyName = PAYMENT_FAILURE_ABORTED
+
+[system_report_payment_payment_failure]
+tableName = v_system_report_payment_payment_failure
+prettyName = PAYMENT_FAILURE
+
+[system_report_payment_success]
+tableName = v_system_report_payment_success
+prettyName = SUCCESS
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/cancellations_per_day.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/cancellations_per_day.sql
new file mode 100644
index 0000000..1bba486
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/cancellations_per_day.sql
@@ -0,0 +1,10 @@
+create or replace view v_cancellations_per_day as
+select
+  date_format(next_start_date, '%Y-%m-%d') as day
+, count(*) as count
+from bst
+where event = 'CANCEL_BASE'
+and report_group = 'default'
+group by 1
+order by 1 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/chargebacks_per_day.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/chargebacks_per_day.sql
new file mode 100644
index 0000000..d67770f
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/chargebacks_per_day.sql
@@ -0,0 +1,10 @@
+create or replace view v_chargebacks_per_day as
+select
+  currency as pivot
+, date_format(created_date, '%Y-%m-%d') as day
+, sum(amount) as count
+from bipc
+where report_group = 'default'
+group by 1, 2
+order by 1, 2 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/conversions_per_day.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/conversions_per_day.sql
new file mode 100644
index 0000000..d6de2c6
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/conversions_per_day.sql
@@ -0,0 +1,11 @@
+create or replace view v_conversions_per_day as
+select
+  date_format(next_start_date, '%Y-%m-%d') as day
+, count(*) as count
+from bst
+where prev_phase = 'TRIAL'
+and next_phase != 'TRIAL'
+and report_group = 'default'
+group by 1
+order by 1 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_adjustments_per_day.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_adjustments_per_day.sql
new file mode 100644
index 0000000..038f4e8
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_adjustments_per_day.sql
@@ -0,0 +1,10 @@
+create or replace view v_invoice_adjustments_per_day as
+select
+  currency as pivot
+, date_format(created_date, '%Y-%m-%d') as day
+, sum(amount) as count
+from bia
+where report_group = 'default'
+group by 1, 2
+order by 1, 2 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_item_adjustments_per_day.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_item_adjustments_per_day.sql
new file mode 100644
index 0000000..61e5eb6
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_item_adjustments_per_day.sql
@@ -0,0 +1,10 @@
+create or replace view v_invoice_item_adjustments_per_day as
+select
+  currency as pivot
+, date_format(created_date, '%Y-%m-%d') as day
+, sum(amount) as count
+from biia
+where report_group = 'default'
+group by 1, 2
+order by 1, 2 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_item_credits_per_day.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_item_credits_per_day.sql
new file mode 100644
index 0000000..b933bfb
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_item_credits_per_day.sql
@@ -0,0 +1,10 @@
+create or replace view v_invoice_item_credits_per_day as
+select
+  currency as pivot
+, date_format(created_date, '%Y-%m-%d') as day
+, sum(amount) as count
+from biic
+where report_group = 'default'
+group by 1, 2
+order by 1, 2 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/invoices_per_day.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/invoices_per_day.sql
new file mode 100644
index 0000000..d353839
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/invoices_per_day.sql
@@ -0,0 +1,10 @@
+create or replace view v_invoices_per_day as
+select
+  currency as pivot
+, date_format(created_date, '%Y-%m-%d') as day
+, sum(original_amount_charged) as count
+from bin
+where report_group = 'default'
+group by 1, 2
+order by 1, 2 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/new_accounts_per_day.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/new_accounts_per_day.sql
new file mode 100644
index 0000000..1b2e0e4
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/new_accounts_per_day.sql
@@ -0,0 +1,9 @@
+create or replace view v_new_accounts_per_day as
+select
+  date_format(created_date, '%Y-%m-%d') as day
+, count(*) as count
+from bac
+where report_group = 'default'
+group by 1
+order by 1 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/new_trials_last_24_hours.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/new_trials_last_24_hours.sql
new file mode 100644
index 0000000..aef9c0d
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/new_trials_last_24_hours.sql
@@ -0,0 +1,14 @@
+create or replace view v_new_trials_last_24_hours as
+select
+  next_slug as pivot
+, date_format(next_start_date, '%Y-%m-%dT%H:00:00Z') as day
+, count(*) as count
+from bst
+where next_start_date > date_sub(curdate(), interval 24 hour)
+and next_start_date <= curdate()
+and event = 'ADD_BASE'
+and next_phase = 'TRIAL'
+and report_group = 'default'
+group by 1, 2
+order by 1, 2 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/new_trials_per_day.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/new_trials_per_day.sql
new file mode 100644
index 0000000..43d4576
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/new_trials_per_day.sql
@@ -0,0 +1,11 @@
+create or replace view v_new_trials_per_day as
+select
+  date_format(next_start_date, '%Y-%m-%d') as day
+, count(*) as count
+from bst
+where event = 'ADD_BASE'
+and next_phase = 'TRIAL'
+and report_group = 'default'
+group by 1
+order by 1 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/payments_per_day.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/payments_per_day.sql
new file mode 100644
index 0000000..5a8d288
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/payments_per_day.sql
@@ -0,0 +1,10 @@
+create or replace view v_payments_per_day as
+select
+  currency as pivot
+, date_format(created_date, '%Y-%m-%d') as day
+, sum(amount) as count
+from bip
+where report_group = 'default'
+group by 1, 2
+order by 1, 2 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/refunds_per_day.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/refunds_per_day.sql
new file mode 100644
index 0000000..5e028b6
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/refunds_per_day.sql
@@ -0,0 +1,10 @@
+create or replace view v_refunds_per_day as
+select
+  currency as pivot
+, date_format(created_date, '%Y-%m-%d') as day
+, sum(amount) as count
+from bipr
+where report_group = 'default'
+group by 1, 2
+order by 1, 2 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_notifications_per_queue_name.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_notifications_per_queue_name.sql
new file mode 100644
index 0000000..fe6947d
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_notifications_per_queue_name.sql
@@ -0,0 +1,10 @@
+create or replace view v_system_report_notifications_per_queue_name as
+select
+  queue_name as pivot
+, date_format(effective_date, '%Y-%m-%d') as day
+, count(*) as count
+from notifications
+where processing_state = 'AVAILABLE'
+group by 1, 2
+order by 1, 2 asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_payment_failure.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_payment_failure.sql
new file mode 100644
index 0000000..a13b6b8
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_payment_failure.sql
@@ -0,0 +1,11 @@
+create or replace view v_system_report_payment_payment_failure as
+select
+  date_format(updated_date, '%Y-%m-%d') as day
+, count(*) as count
+from payments
+join bin using(invoice_id)
+where bin.balance > 0
+and payment_status = 'PAYMENT_FAILURE'
+group by date_format(updated_date, '%Y-%m-%d')
+order by date_format(updated_date, '%Y-%m-%d') asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_payment_failure_aborted.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_payment_failure_aborted.sql
new file mode 100644
index 0000000..7d7e8d1
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_payment_failure_aborted.sql
@@ -0,0 +1,11 @@
+create or replace view v_system_report_payment_payment_failure_aborted as
+select
+  date_format(updated_date, '%Y-%m-%d') as day
+, count(*) as count
+from payments
+join bin using(invoice_id)
+where bin.balance > 0
+and payment_status = 'PAYMENT_FAILURE_ABORTED'
+group by date_format(updated_date, '%Y-%m-%d')
+order by date_format(updated_date, '%Y-%m-%d') asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_plugin_failure.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_plugin_failure.sql
new file mode 100644
index 0000000..81f2f87
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_plugin_failure.sql
@@ -0,0 +1,11 @@
+create or replace view v_system_report_payment_plugin_failure as
+select
+  date_format(updated_date, '%Y-%m-%d') as day
+, count(*) as count
+from payments
+join bin using(invoice_id)
+where bin.balance > 0
+and payment_status = 'PLUGIN_FAILURE'
+group by date_format(updated_date, '%Y-%m-%d')
+order by date_format(updated_date, '%Y-%m-%d') asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_plugin_failure_aborted.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_plugin_failure_aborted.sql
new file mode 100644
index 0000000..fa0ee08
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_plugin_failure_aborted.sql
@@ -0,0 +1,11 @@
+create or replace view v_system_report_payment_plugin_failure_aborted as
+select
+  date_format(updated_date, '%Y-%m-%d') as day
+, count(*) as count
+from payments
+join bin using(invoice_id)
+where bin.balance > 0
+and (payment_status = 'PLUGIN_FAILURE_ABORTED' or payment_status = 'UNKNOWN')
+group by date_format(updated_date, '%Y-%m-%d')
+order by date_format(updated_date, '%Y-%m-%d') asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_success.sql b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_success.sql
new file mode 100644
index 0000000..78b542d
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_success.sql
@@ -0,0 +1,11 @@
+create or replace view v_system_report_payment_success as
+select
+  date_format(updated_date, '%Y-%m-%d') as day
+, count(*) as count
+from payments
+join bin using(invoice_id)
+where bin.balance > 0
+and payment_status = 'SUCCESS'
+group by date_format(updated_date, '%Y-%m-%d')
+order by date_format(updated_date, '%Y-%m-%d') asc
+;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/static/analytics.html b/osgi-bundles/bundles/analytics/src/main/resources/static/analytics.html
new file mode 100644
index 0000000..c4759e2
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/static/analytics.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<!--
+  ~ Copyright 2010-2013 Ning, Inc.
+  ~
+  ~ Ning licenses this file to you 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.
+  -->
+
+<html lang="en">
+<head>
+    <meta charset="utf-8"/>
+    <title>Kill Bill analytics dashboards</title>
+
+    <script type="text/javascript" src="javascript/d3.js"></script>
+    <script type="text/javascript" src="javascript/jquery-1.9.0.min.js"></script>
+    <script type="text/javascript" src="javascript/purl.js"></script>
+    <script type="text/javascript" src="javascript/killbill.js"></script>
+
+    <link rel="stylesheet" type="text/css" href="styles/dashboard.css" media="screen"/>
+</head>
+<body>
+    <div id="chartAnchor"></div>
+
+    <script type="text/javascript">
+        function createLinesGraph(data, graphStructure, canvas, input, canvasHeightGraph, translateX, translateY, translateLabelY) {
+          console.log(data);
+          var canvasGroup = graphStructure.createCanvasGroup(canvas, translateX, translateY);
+          var linesGraph = new killbillGraph.KBLinesGraph(canvasGroup, data, input.canvasWidth, canvasHeightGraph, d3.scale.category20b());
+          linesGraph.drawLines();
+          linesGraph.addLabels("labelsLine", translateLabelY);
+          linesGraph.addMouseOverCircleForValue();
+          return linesGraph;
+        }
+
+        function drawAll(input, dataForAllReports) {
+          var graphStructure = new killbillGraph.GraphStructure();
+          graphStructure.setupDomStructure();
+
+          // Positions
+          var canvasHeightWithMargins = input.canvasHeigth + input.topMargin + input.betweenGraphMargin + input.bottomMargin;
+          var canvas = graphStructure.createCanvas([input.topMargin, input.rightMargin, input.bottomMargin, input.leftMargin], input.canvasWidth, canvasHeightWithMargins);
+          var canvasHeightGraph = input.canvasHeigth / 2;
+          var translateX = input.leftMargin;
+          var translateY = input.topMargin;
+          var translateLabelY = translateY + (canvasHeightGraph / 2);
+
+          for (var idx in dataForAllReports) {
+            var lastGraph = createLinesGraph(dataForAllReports[idx], graphStructure, canvas, input, canvasHeightGraph, translateX, translateY, translateLabelY);
+            translateY = translateY + canvasHeightGraph + input.betweenGraphMargin;
+            translateLabelY = translateLabelY + input.betweenGraphMargin;
+          }
+
+          // Bottom, shared, X axis
+          var xAxisCanvasGroup = graphStructure.createCanvasGroup(canvas, translateX, translateY);
+          lastGraph.createXAxis(xAxisCanvasGroup, dataForAllReports.length * (canvasHeightGraph + input.betweenGraphMargin));
+        }
+
+        // Get the data for a set of reports
+        function doGetData(position, reports, from, to, smoothFunction, fn) {
+          var request_url = "http://" + $VAR_SERVER + ":" + $VAR_PORT + "/plugins/killbill-analytics/reports?startDate=" + from + "&endDate=" + to + "&name=" + reports.join("&name=");
+          if (smoothFunction) {
+            request_url = request_url + "&smooth=" + smoothFunction;
+          }
+
+          return $.ajax({
+            type: "GET",
+            contentType: "application/json",
+            dataType: "json",
+            url: request_url
+          }).done(function(data) { console.log(typeof data); fn(position, reports, data); })
+            .fail(function(jqXHR, textStatus) { alert("Request failed: " + textStatus); });
+        }
+
+        // The URL structure is expected to be in the form: analytics.html?report1=new_trials_per_day&report1=cancellations_per_day&report2=conversions_per_day
+        $(document).ready(function() {
+          // Set (sane?) default values for from and to if unspecified. This is to make sure all graphs will share the exact same X axis (the server will normalize the data)
+          var now = new Date();
+          var from = $.url().param('startDate');
+          if (!from) {
+            from = now.getFullYear() + '-' + (now.getMonth() - 3) + '-' + now.getDay();
+          }
+          var to = $.url().param('endDate');
+          if (!to) {
+            to = now.getFullYear() + '-' + (now.getMonth() + 3) + '-' + now.getDay();
+          }
+
+          // Map of position (starting from the top) to an array of reports
+          var reports = {}
+          // Map of position (starting from the top) to a smoothing function
+          var smoothFunctions = {}
+          for (var i = 1; i < 10; i++) {
+            var reportsI = $.url().param('report' + i);
+            if (!reportsI) {
+              // No more reports
+              break;
+            } else if (reportsI instanceof Array) {
+              reports[i] = reportsI;
+            } else {
+              reports[i] = [reportsI];
+            }
+
+            smoothFunctions[i] = $.url().param('smooth' + i);
+          }
+
+          // Set sane defaults
+          if (!reports[1]) {
+            // Built-in set of reports
+            reports[1] = ["new_trials_per_day", "cancellations_per_day", "conversions_per_day"];
+          }
+
+          // Array of all deferreds
+          var futures = []
+          // Map of position (starting from the top) to the data
+          var futuresData = {}
+          for (var position in reports) {
+            // Fetch the data
+            var future = doGetData(position, reports[position], from, to, smoothFunctions[position], function(zePosition, reports, reportsData) {
+              console.log(typeof reportsData);
+              if (!(reportsData instanceof Array) || reportsData.length == 0) {
+                futuresData[zePosition] = [ { "name": "No data", "values": [] } ];
+              } else {
+                futuresData[zePosition] = reportsData;
+              }
+            });
+            futures.push(future);
+          }
+
+          $.when.apply(null, futures).done(function() {
+            var dataForAllReports = [];
+            for (var position in reports) {
+              // Index starts at zero
+              dataForAllReports[position - 1] = futuresData[position];
+            }
+
+            var input = new killbillGraph.KBInputGraphs(800, 600, 80, 80, 80, 80, 30, dataForAllReports);
+            drawAll(input, dataForAllReports);
+          });
+        });
+    </script>
+</body>
+</html>
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/static/javascript/purl.js b/osgi-bundles/bundles/analytics/src/main/resources/static/javascript/purl.js
new file mode 100644
index 0000000..509ca08
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/resources/static/javascript/purl.js
@@ -0,0 +1,271 @@
+/*
+ * JQuery URL Parser plugin, v2.2.1
+ * Developed and maintanined by Mark Perkins, mark@allmarkedup.com
+ * Source repository: https://github.com/allmarkedup/jQuery-URL-Parser
+ * Licensed under an MIT-style license. See https://github.com/allmarkedup/jQuery-URL-Parser/blob/master/LICENSE for details.
+ */ 
+
+;(function(factory) {
+	if (typeof define === 'function' && define.amd) {
+		// AMD available; use anonymous module
+		if ( typeof jQuery !== 'undefined' ) {
+			define(['jquery'], factory);	
+		} else {
+			define([], factory);
+		}
+	} else {
+		// No AMD available; mutate global vars
+		if ( typeof jQuery !== 'undefined' ) {
+			factory(jQuery);
+		} else {
+			factory();
+		}
+	}
+})(function($, undefined) {
+	
+	var tag2attr = {
+			a       : 'href',
+			img     : 'src',
+			form    : 'action',
+			base    : 'href',
+			script  : 'src',
+			iframe  : 'src',
+			link    : 'href'
+		},
+		
+		key = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'fragment'], // keys available to query
+		
+		aliases = { 'anchor' : 'fragment' }, // aliases for backwards compatability
+		
+		parser = {
+			strict : /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,  //less intuitive, more accurate to the specs
+			loose :  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ // more intuitive, fails on relative paths and deviates from specs
+		},
+		
+		toString = Object.prototype.toString,
+		
+		isint = /^[0-9]+$/;
+	
+	function parseUri( url, strictMode ) {
+		var str = decodeURI( url ),
+		res   = parser[ strictMode || false ? 'strict' : 'loose' ].exec( str ),
+		uri = { attr : {}, param : {}, seg : {} },
+		i   = 14;
+		
+		while ( i-- ) {
+			uri.attr[ key[i] ] = res[i] || '';
+		}
+		
+		// build query and fragment parameters		
+		uri.param['query'] = parseString(uri.attr['query']);
+		uri.param['fragment'] = parseString(uri.attr['fragment']);
+		
+		// split path and fragement into segments		
+		uri.seg['path'] = uri.attr.path.replace(/^\/+|\/+$/g,'').split('/');     
+		uri.seg['fragment'] = uri.attr.fragment.replace(/^\/+|\/+$/g,'').split('/');
+		
+		// compile a 'base' domain attribute        
+		uri.attr['base'] = uri.attr.host ? (uri.attr.protocol ?  uri.attr.protocol+'://'+uri.attr.host : uri.attr.host) + (uri.attr.port ? ':'+uri.attr.port : '') : '';      
+		  
+		return uri;
+	};
+	
+	function getAttrName( elm ) {
+		var tn = elm.tagName;
+		if ( typeof tn !== 'undefined' ) return tag2attr[tn.toLowerCase()];
+		return tn;
+	}
+	
+	function promote(parent, key) {
+		if (parent[key].length == 0) return parent[key] = {};
+		var t = {};
+		for (var i in parent[key]) t[i] = parent[key][i];
+		parent[key] = t;
+		return t;
+	}
+
+	function parse(parts, parent, key, val) {
+		var part = parts.shift();
+		if (!part) {
+			if (isArray(parent[key])) {
+				parent[key].push(val);
+			} else if ('object' == typeof parent[key]) {
+				parent[key] = val;
+			} else if ('undefined' == typeof parent[key]) {
+				parent[key] = val;
+			} else {
+				parent[key] = [parent[key], val];
+			}
+		} else {
+			var obj = parent[key] = parent[key] || [];
+			if (']' == part) {
+				if (isArray(obj)) {
+					if ('' != val) obj.push(val);
+				} else if ('object' == typeof obj) {
+					obj[keys(obj).length] = val;
+				} else {
+					obj = parent[key] = [parent[key], val];
+				}
+			} else if (~part.indexOf(']')) {
+				part = part.substr(0, part.length - 1);
+				if (!isint.test(part) && isArray(obj)) obj = promote(parent, key);
+				parse(parts, obj, part, val);
+				// key
+			} else {
+				if (!isint.test(part) && isArray(obj)) obj = promote(parent, key);
+				parse(parts, obj, part, val);
+			}
+		}
+	}
+
+	function merge(parent, key, val) {
+		if (~key.indexOf(']')) {
+			var parts = key.split('['),
+			len = parts.length,
+			last = len - 1;
+			parse(parts, parent, 'base', val);
+		} else {
+			if (!isint.test(key) && isArray(parent.base)) {
+				var t = {};
+				for (var k in parent.base) t[k] = parent.base[k];
+				parent.base = t;
+			}
+			set(parent.base, key, val);
+		}
+		return parent;
+	}
+
+	function parseString(str) {
+		return reduce(String(str).split(/&|;/), function(ret, pair) {
+			try {
+				pair = decodeURIComponent(pair.replace(/\+/g, ' '));
+			} catch(e) {
+				// ignore
+			}
+			var eql = pair.indexOf('='),
+				brace = lastBraceInKey(pair),
+				key = pair.substr(0, brace || eql),
+				val = pair.substr(brace || eql, pair.length),
+				val = val.substr(val.indexOf('=') + 1, val.length);
+
+			if ('' == key) key = pair, val = '';
+
+			return merge(ret, key, val);
+		}, { base: {} }).base;
+	}
+	
+	function set(obj, key, val) {
+		var v = obj[key];
+		if (undefined === v) {
+			obj[key] = val;
+		} else if (isArray(v)) {
+			v.push(val);
+		} else {
+			obj[key] = [v, val];
+		}
+	}
+	
+	function lastBraceInKey(str) {
+		var len = str.length,
+			 brace, c;
+		for (var i = 0; i < len; ++i) {
+			c = str[i];
+			if (']' == c) brace = false;
+			if ('[' == c) brace = true;
+			if ('=' == c && !brace) return i;
+		}
+	}
+	
+	function reduce(obj, accumulator){
+		var i = 0,
+			l = obj.length >> 0,
+			curr = arguments[2];
+		while (i < l) {
+			if (i in obj) curr = accumulator.call(undefined, curr, obj[i], i, obj);
+			++i;
+		}
+		return curr;
+	}
+	
+	function isArray(vArg) {
+		return Object.prototype.toString.call(vArg) === "[object Array]";
+	}
+	
+	function keys(obj) {
+		var keys = [];
+		for ( prop in obj ) {
+			if ( obj.hasOwnProperty(prop) ) keys.push(prop);
+		}
+		return keys;
+	}
+		
+	function purl( url, strictMode ) {
+		if ( arguments.length === 1 && url === true ) {
+			strictMode = true;
+			url = undefined;
+		}
+		strictMode = strictMode || false;
+		url = url || window.location.toString();
+	
+		return {
+			
+			data : parseUri(url, strictMode),
+			
+			// get various attributes from the URI
+			attr : function( attr ) {
+				attr = aliases[attr] || attr;
+				return typeof attr !== 'undefined' ? this.data.attr[attr] : this.data.attr;
+			},
+			
+			// return query string parameters
+			param : function( param ) {
+				return typeof param !== 'undefined' ? this.data.param.query[param] : this.data.param.query;
+			},
+			
+			// return fragment parameters
+			fparam : function( param ) {
+				return typeof param !== 'undefined' ? this.data.param.fragment[param] : this.data.param.fragment;
+			},
+			
+			// return path segments
+			segment : function( seg ) {
+				if ( typeof seg === 'undefined' ) {
+					return this.data.seg.path;
+				} else {
+					seg = seg < 0 ? this.data.seg.path.length + seg : seg - 1; // negative segments count from the end
+					return this.data.seg.path[seg];                    
+				}
+			},
+			
+			// return fragment segments
+			fsegment : function( seg ) {
+				if ( typeof seg === 'undefined' ) {
+					return this.data.seg.fragment;                    
+				} else {
+					seg = seg < 0 ? this.data.seg.fragment.length + seg : seg - 1; // negative segments count from the end
+					return this.data.seg.fragment[seg];                    
+				}
+			}
+	    	
+		};
+	
+	};
+	
+	if ( typeof $ !== 'undefined' ) {
+		
+		$.fn.url = function( strictMode ) {
+			var url = '';
+			if ( this.length ) {
+				url = $(this).attr( getAttrName(this[0]) ) || '';
+			}    
+			return purl( url, strictMode );
+		};
+		
+		$.url = purl;
+		
+	} else {
+		window.purl = purl;
+	}
+
+});
+
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/api/user/TestDefaultAnalyticsUserApi.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/api/user/TestDefaultAnalyticsUserApi.java
index 7cbae91..3c80ea2 100644
--- a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/api/user/TestDefaultAnalyticsUserApi.java
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/api/user/TestDefaultAnalyticsUserApi.java
@@ -42,7 +42,7 @@ public class TestDefaultAnalyticsUserApi extends AnalyticsTestSuiteWithEmbeddedD
                                                                                     reportGroup);
         analyticsSqlDao.create(accountModelDao.getTableName(), accountModelDao, callContext);
 
-        final AnalyticsUserApi analyticsUserApi = new AnalyticsUserApi(logService, killbillAPI, killbillDataSource, BusinessExecutor.create(logService));
+        final AnalyticsUserApi analyticsUserApi = new AnalyticsUserApi(logService, killbillAPI, killbillDataSource, BusinessExecutor.newCachedThreadPool());
         final BusinessSnapshot businessSnapshot = analyticsUserApi.getBusinessSnapshot(account.getId(), callContext);
         Assert.assertEquals(businessSnapshot.getBusinessAccount(), new BusinessAccount(accountModelDao));
     }
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/factory/TestBusinessBundleSummaryFactory.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/factory/TestBusinessBundleSummaryFactory.java
index ca99580..ae5e42e 100644
--- a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/factory/TestBusinessBundleSummaryFactory.java
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/factory/TestBusinessBundleSummaryFactory.java
@@ -72,7 +72,7 @@ public class TestBusinessBundleSummaryFactory extends AnalyticsTestSuiteNoDB {
             }
         }).when(osgiKillbillLogService).log(Mockito.anyInt(), Mockito.anyString());
 
-        bundleSummaryDao = new BusinessBundleSummaryFactory(osgiKillbillLogService, null, BusinessExecutor.create(osgiKillbillLogService));
+        bundleSummaryDao = new BusinessBundleSummaryFactory(osgiKillbillLogService, null, BusinessExecutor.newCachedThreadPool());
     }
 
     @Test(groups = "fast")
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/factory/TestBusinessInvoiceFactory.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/factory/TestBusinessInvoiceFactory.java
index 77bfc4e..6f45b5e 100644
--- a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/factory/TestBusinessInvoiceFactory.java
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/factory/TestBusinessInvoiceFactory.java
@@ -64,7 +64,7 @@ public class TestBusinessInvoiceFactory extends AnalyticsTestSuiteNoDB {
             }
         }).when(osgiKillbillLogService).log(Mockito.anyInt(), Mockito.anyString());
 
-        invoiceFactory = new BusinessInvoiceFactory(osgiKillbillLogService, null, BusinessExecutor.create(osgiKillbillLogService));
+        invoiceFactory = new BusinessInvoiceFactory(osgiKillbillLogService, null, BusinessExecutor.newCachedThreadPool());
     }
 
     @Test(groups = "fast")
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/http/TestAnalyticsServlet.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/http/TestAnalyticsServlet.java
index f0a92df..5065846 100644
--- a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/http/TestAnalyticsServlet.java
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/http/TestAnalyticsServlet.java
@@ -62,7 +62,7 @@ public class TestAnalyticsServlet {
         mapper.writeValue(writer, res);
 
         Assert.assertEquals(writer.toString(),
-                            "[{\"name\":\"serie1\",\"values\":[{\"x\":\"2013-01-01\",\"y\":11},{\"x\":\"2013-01-02\",\"y\":7},{\"x\":\"2013-01-03\",\"y\":34}]},{\"name\":\"serie2\",\"values\":[{\"x\":\"2013-01-01\",\"y\":12},{\"x\":\"2013-01-02\",\"y\":5},{\"x\":\"2013-01-03\",\"y\":3}]}]");
+                            "[{\"name\":\"serie1\",\"values\":[{\"x\":\"2013-01-01\",\"y\":11.0},{\"x\":\"2013-01-02\",\"y\":7.0},{\"x\":\"2013-01-03\",\"y\":34.0}]},{\"name\":\"serie2\",\"values\":[{\"x\":\"2013-01-01\",\"y\":12.0},{\"x\":\"2013-01-02\",\"y\":5.0},{\"x\":\"2013-01-03\",\"y\":3.0}]}]");
 
     }
 
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/TestBusinessExecutor.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/TestBusinessExecutor.java
index 947c336..f779d83 100644
--- a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/TestBusinessExecutor.java
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/TestBusinessExecutor.java
@@ -19,6 +19,7 @@ package com.ning.billing.osgi.bundles.analytics;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CompletionService;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorCompletionService;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -50,7 +51,7 @@ public class TestBusinessExecutor extends AnalyticsTestSuiteNoDB {
 
     @Test(groups = "fast")
     public void testRejectionPolicy() throws Exception {
-        final BusinessExecutor executor = BusinessExecutor.create(logService);
+        final Executor executor = BusinessExecutor.newCachedThreadPool();
         final CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executor);
 
         final int totalTasksSize = BusinessExecutor.NB_THREADS * 50;
diff --git a/osgi-bundles/bundles/jruby/pom.xml b/osgi-bundles/bundles/jruby/pom.xml
index 4d18427..f99da59 100644
--- a/osgi-bundles/bundles/jruby/pom.xml
+++ b/osgi-bundles/bundles/jruby/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-bundles-jruby</artifactId>
@@ -43,6 +43,10 @@
             <artifactId>killbill-osgi-bundles-lib-killbill</artifactId>
         </dependency>
         <dependency>
+            <groupId>com.ning.billing.commons</groupId>
+            <artifactId>killbill-concurrent</artifactId>
+        </dependency>
+        <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
         </dependency>
diff --git a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyActivator.java b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyActivator.java
index 0e5cd9a..e98b1ab 100644
--- a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyActivator.java
+++ b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyActivator.java
@@ -16,14 +16,17 @@
 
 package com.ning.billing.osgi.bundles.jruby;
 
-import java.util.Collections;
+import java.io.File;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
-import org.jruby.embed.ScriptingContainer;
 import org.osgi.framework.BundleContext;
 import org.osgi.service.log.LogService;
 
+import com.ning.billing.commons.concurrent.Executors;
 import com.ning.billing.osgi.api.config.PluginConfig.PluginType;
 import com.ning.billing.osgi.api.config.PluginConfigServiceApi;
 import com.ning.billing.osgi.api.config.PluginRubyConfig;
@@ -34,21 +37,24 @@ import com.google.common.base.Objects;
 
 public class JRubyActivator extends KillbillActivatorBase {
 
-    private static final String jrubyPluginsConfDir = System.getProperty("com.ning.billing.osgi.bundles.jruby.conf.dir");
+    private static final String JRUBY_PLUGINS_CONF_DIR = System.getProperty("com.ning.billing.osgi.bundles.jruby.conf.dir");
+    private static final int JRUBY_PLUGINS_RESTART_DELAY_SECS = Integer.parseInt(System.getProperty("com.ning.billing.osgi.bundles.jruby.restart.delay.secs", "5"));
+
+    private static final String TMP_DIR_NAME = "tmp";
+    private static final String RESTART_FILE_NAME = "restart.txt";
 
     private JRubyPlugin plugin = null;
+    private ScheduledFuture<?> restartFuture = null;
 
-    private final static String KILLBILL_PLUGIN_JPAYMENT = "Killbill::Plugin::JPayment";
-    private final static String KILLBILL_PLUGIN_JNOTIFICATION = "Killbill::Plugin::JNotification";
+    private static final String KILLBILL_PLUGIN_JPAYMENT = "Killbill::Plugin::JPayment";
+    private static final String KILLBILL_PLUGIN_JNOTIFICATION = "Killbill::Plugin::JNotification";
 
     public void start(final BundleContext context) throws Exception {
-
         super.start(context);
 
         withContextClassLoader(new PluginCall() {
             @Override
             public void doCall() {
-
                 logService.log(LogService.LOG_INFO, "JRuby bundle activated");
 
                 // Retrieve the plugin config
@@ -56,32 +62,75 @@ public class JRubyActivator extends KillbillActivatorBase {
 
                 // Setup JRuby
                 final String pluginMain;
-                final ScriptingContainer scriptingContainer = setupScriptingContainer(rubyConfig);
                 if (PluginType.NOTIFICATION.equals(rubyConfig.getPluginType())) {
-                    plugin = new JRubyNotificationPlugin(rubyConfig, scriptingContainer, context, logService);
+                    plugin = new JRubyNotificationPlugin(rubyConfig, context, logService);
                     dispatcher.registerEventHandler((OSGIKillbillEventHandler) plugin);
                     pluginMain = KILLBILL_PLUGIN_JNOTIFICATION;
                 } else if (PluginType.PAYMENT.equals(rubyConfig.getPluginType())) {
-                    plugin = new JRubyPaymentPlugin(rubyConfig, scriptingContainer, context, logService);
+                    plugin = new JRubyPaymentPlugin(rubyConfig, context, logService);
                     pluginMain = KILLBILL_PLUGIN_JPAYMENT;
                 } else {
                     throw new IllegalStateException("Unsupported plugin type " + rubyConfig.getPluginType());
                 }
 
                 // Validate and instantiate the plugin
+                startPlugin(rubyConfig, pluginMain, context);
+            }
+        }, this.getClass().getClassLoader());
+    }
+
+    private void startPlugin(final PluginRubyConfig rubyConfig, final String pluginMain, final BundleContext context) {
+        final Map<String, Object> killbillServices = retrieveKillbillApis(context);
+        killbillServices.put("root", rubyConfig.getPluginVersionRoot().getAbsolutePath());
+        killbillServices.put("logger", logService);
+        // Default to the plugin root dir if no jruby plugins specific configuration directory was specified
+        killbillServices.put("conf_dir", Objects.firstNonNull(JRUBY_PLUGINS_CONF_DIR, rubyConfig.getPluginVersionRoot().getAbsolutePath()));
+
+        // Setup the restart mechanism. This is useful for hotswapping plugin code
+        // The principle is similar to the one in Phusion Passenger:
+        // http://www.modrails.com/documentation/Users%20guide%20Apache.html#_redeploying_restarting_the_ruby_on_rails_application
+        final File tmpDirPath = new File(rubyConfig.getPluginVersionRoot().getAbsolutePath() + "/" + TMP_DIR_NAME);
+        if (!tmpDirPath.exists()) {
+            if (!tmpDirPath.mkdir()) {
+                logService.log(LogService.LOG_WARNING, "Unable to create directory " + tmpDirPath + ", the restart mechanism is disabled");
+                return;
+            }
+        }
+        if (!tmpDirPath.isDirectory()) {
+            logService.log(LogService.LOG_WARNING, tmpDirPath + " is not a directory, the restart mechanism is disabled");
+            return;
+        }
 
-                final Map<String, Object> killbillServices = retrieveKillbillApis(context);
-                killbillServices.put("root", rubyConfig.getPluginVersionRoot().getAbsolutePath());
-                killbillServices.put("logger", logService);
-                // Default to the plugin root dir if no jruby plugins specific configuration directory was specified
-                killbillServices.put("conf_dir", Objects.firstNonNull(jrubyPluginsConfDir, rubyConfig.getPluginVersionRoot().getAbsolutePath()));
-                plugin.instantiatePlugin(killbillServices, pluginMain);
+        final AtomicBoolean firstStart = new AtomicBoolean(true);
+        restartFuture = Executors.newSingleThreadScheduledExecutor("jruby-restarter-" + pluginMain)
+                                 .scheduleWithFixedDelay(new Runnable() {
+            long lastRestartMillis = System.currentTimeMillis();
 
-                logService.log(LogService.LOG_INFO, "Starting JRuby plugin " + plugin.getPluginMainClass());
-                plugin.startPlugin(context);
+            @Override
+            public void run() {
+                if (firstStart.get()) {
+                    // Initial start
+                    logService.log(LogService.LOG_INFO, "Starting JRuby plugin " + rubyConfig.getRubyMainClass());
+                    doStartPlugin(pluginMain, context, killbillServices);
+                    firstStart.set(false);
+                    return;
+                }
+
+                final File restartFile = new File(tmpDirPath + "/" + RESTART_FILE_NAME);
+                if (!restartFile.isFile()) {
+                    return;
+                }
 
+                if (restartFile.lastModified() > lastRestartMillis) {
+                    logService.log(LogService.LOG_INFO, "Restarting JRuby plugin " + rubyConfig.getRubyMainClass());
+
+                    doStopPlugin(context);
+                    doStartPlugin(pluginMain, context, killbillServices);
+
+                    lastRestartMillis = restartFile.lastModified();
+                }
             }
-        }, this.getClass().getClassLoader());
+        }, 0, JRUBY_PLUGINS_RESTART_DELAY_SECS, TimeUnit.SECONDS);
     }
 
     private PluginRubyConfig retrievePluginRubyConfig(final BundleContext context) {
@@ -89,27 +138,29 @@ public class JRubyActivator extends KillbillActivatorBase {
         return pluginConfigServiceApi.getPluginRubyConfig(context.getBundle().getBundleId());
     }
 
-    private ScriptingContainer setupScriptingContainer(final PluginRubyConfig rubyConfig) {
-        final ScriptingContainer scriptingContainer = new ScriptingContainer();
-
-        // Set the load paths instead of adding, to avoid looking at the filesystem
-        scriptingContainer.setLoadPaths(Collections.<String>singletonList(rubyConfig.getRubyLoadDir()));
-
-        return scriptingContainer;
-    }
-
     public void stop(final BundleContext context) throws Exception {
 
         withContextClassLoader(new PluginCall() {
             @Override
             public void doCall() {
-                plugin.stopPlugin(context);
+                restartFuture.cancel(true);
+                doStopPlugin(context);
                 killbillAPI.close();
                 logService.close();
             }
         }, this.getClass().getClassLoader());
     }
 
+    private void doStartPlugin(final String pluginMain, final BundleContext context, final Map<String, Object> killbillServices) {
+        plugin.instantiatePlugin(killbillServices, pluginMain);
+        plugin.startPlugin(context);
+    }
+
+    private void doStopPlugin(final BundleContext context) {
+        plugin.stopPlugin(context);
+        plugin.unInstantiatePlugin();
+    }
+
     // We make the explicit registration in the start method by hand as this would be called too early
     // (see OSGIKillbillEventDispatcher)
     @Override
@@ -145,6 +196,7 @@ public class JRubyActivator extends KillbillActivatorBase {
 
 
     private static interface PluginCall {
+
         public void doCall();
     }
 
diff --git a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java
index 18cddd7..41712a7 100644
--- a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java
+++ b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java
@@ -17,33 +17,23 @@
 package com.ning.billing.osgi.bundles.jruby;
 
 import org.jruby.Ruby;
-import org.jruby.embed.ScriptingContainer;
-import org.jruby.javasupport.JavaEmbedUtils;
 import org.osgi.framework.BundleContext;
 import org.osgi.service.log.LogService;
 
 import com.ning.billing.beatrix.bus.api.ExtBusEvent;
 import com.ning.billing.notification.plugin.api.NotificationPluginApi;
 import com.ning.billing.osgi.api.config.PluginRubyConfig;
-import com.ning.billing.payment.plugin.api.PaymentPluginApi;
 import com.ning.billing.payment.plugin.api.PaymentPluginApiException;
 import com.ning.killbill.osgi.libs.killbill.OSGIKillbillEventDispatcher.OSGIKillbillEventHandler;
 
 public class JRubyNotificationPlugin extends JRubyPlugin implements OSGIKillbillEventHandler {
 
-    public JRubyNotificationPlugin(final PluginRubyConfig config, final ScriptingContainer container,
-                                   final BundleContext bundleContext, final LogService logger) {
-        super(config, container, bundleContext, logger);
-    }
-
-    @Override
-    public void startPlugin(final BundleContext context) {
-        super.startPlugin(context);
+    public JRubyNotificationPlugin(final PluginRubyConfig config, final BundleContext bundleContext, final LogService logger) {
+        super(config, bundleContext, logger);
     }
 
     @Override
     public void handleKillbillEvent(final ExtBusEvent killbillEvent) {
-
         try {
             callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.NOTIFICATION) {
                 @Override
diff --git a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
index 3b52ee3..ae9fecf 100644
--- a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
+++ b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
@@ -23,7 +23,6 @@ import java.util.List;
 import java.util.UUID;
 
 import org.jruby.Ruby;
-import org.jruby.embed.ScriptingContainer;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.log.LogService;
@@ -44,9 +43,8 @@ public class JRubyPaymentPlugin extends JRubyPlugin implements PaymentPluginApi 
 
     private volatile ServiceRegistration<PaymentPluginApi> paymentInfoPluginRegistration;
 
-    public JRubyPaymentPlugin(final PluginRubyConfig config, final ScriptingContainer container,
-                              final BundleContext bundleContext, final LogService logger) {
-        super(config, container, bundleContext, logger);
+    public JRubyPaymentPlugin(final PluginRubyConfig config, final BundleContext bundleContext, final LogService logger) {
+        super(config, bundleContext, logger);
     }
 
     @Override
@@ -62,11 +60,12 @@ public class JRubyPaymentPlugin extends JRubyPlugin implements PaymentPluginApi 
 
     @Override
     public void stopPlugin(final BundleContext context) {
-        paymentInfoPluginRegistration.unregister();
+        if (paymentInfoPluginRegistration != null) {
+            paymentInfoPluginRegistration.unregister();
+        }
         super.stopPlugin(context);
     }
 
-
     @Override
     public PaymentInfoPlugin processPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
 
diff --git a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java
index 9f4f391..d3685eb 100644
--- a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java
+++ b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java
@@ -16,7 +16,7 @@
 
 package com.ning.billing.osgi.bundles.jruby;
 
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.Hashtable;
 import java.util.Map;
 
@@ -25,6 +25,8 @@ import javax.servlet.http.HttpServlet;
 import org.jruby.Ruby;
 import org.jruby.RubyObject;
 import org.jruby.embed.EvalFailedException;
+import org.jruby.embed.LocalContextScope;
+import org.jruby.embed.LocalVariableBehavior;
 import org.jruby.embed.ScriptingContainer;
 import org.jruby.runtime.builtin.IRubyObject;
 import org.osgi.framework.BundleContext;
@@ -39,7 +41,7 @@ import com.ning.billing.payment.plugin.api.PaymentPluginApiException;
 // Bridge between the OSGI bundle and the ruby plugin
 public abstract class JRubyPlugin {
 
-    private final static Logger log = LoggerFactory.getLogger(JRubyPlugin.class);
+    private static final Logger log = LoggerFactory.getLogger(JRubyPlugin.class);
 
     // Killbill gem base classes
     private static final String KILLBILL_PLUGIN_BASE = "Killbill::Plugin::PluginBase";
@@ -50,44 +52,38 @@ public abstract class JRubyPlugin {
     private static final String KILLBILL_SERVICES = "java_apis";
     private static final String KILLBILL_PLUGIN_CLASS_NAME = "plugin_class_name";
 
+    // Methods implemented by Killbill::Plugin::JPlugin
+    private static final String START_PLUGIN_RUBY_METHOD_NAME = "start_plugin";
+    private static final String STOP_PLUGIN_RUBY_METHOD_NAME = "stop_plugin";
+    private static final String RACK_HANDLER_RUBY_METHOD_NAME = "rack_handler";
+
+    private final Object pluginMonitor = new Object();
+
     protected final LogService logger;
     protected final BundleContext bundleContext;
     protected final String pluginGemName;
     protected final String rubyRequire;
     protected final String pluginMainClass;
-    protected final ScriptingContainer container;
     protected final String pluginLibdir;
 
+    protected ScriptingContainer container;
     protected RubyObject pluginInstance;
 
     private ServiceRegistration httpServletServiceRegistration = null;
     private String cachedRequireLine = null;
 
-    public JRubyPlugin(final PluginRubyConfig config, final ScriptingContainer container,
-                       final BundleContext bundleContext, final LogService logger) {
+    public JRubyPlugin(final PluginRubyConfig config, final BundleContext bundleContext, final LogService logger) {
         this.logger = logger;
         this.bundleContext = bundleContext;
         this.pluginGemName = config.getPluginName();
         this.rubyRequire = config.getRubyRequire();
         this.pluginMainClass = config.getRubyMainClass();
-        this.container = container;
         this.pluginLibdir = config.getRubyLoadDir();
-
-        // Path to the gem
-        if (pluginLibdir != null) {
-            container.setLoadPaths(Arrays.asList(pluginLibdir));
-        }
-    }
-
-    public String getPluginMainClass() {
-        return pluginMainClass;
-    }
-
-    public String getPluginLibdir() {
-        return pluginLibdir;
     }
 
     public void instantiatePlugin(final Map<String, Object> killbillApis, final String pluginMain) {
+        container = setupScriptingContainer();
+
         checkValidPlugin();
 
         // Register all killbill APIs
@@ -103,7 +99,7 @@ public abstract class JRubyPlugin {
 
     public void startPlugin(final BundleContext context) {
         checkPluginIsStopped();
-        pluginInstance.callMethod("start_plugin");
+        pluginInstance.callMethod(START_PLUGIN_RUBY_METHOD_NAME);
         checkPluginIsRunning();
         registerHttpServlet();
     }
@@ -111,13 +107,18 @@ public abstract class JRubyPlugin {
     public void stopPlugin(final BundleContext context) {
         checkPluginIsRunning();
         unregisterHttpServlet();
-        pluginInstance.callMethod("stop_plugin");
+        pluginInstance.callMethod(STOP_PLUGIN_RUBY_METHOD_NAME);
         checkPluginIsStopped();
     }
 
+    public void unInstantiatePlugin() {
+        // Cleanup the container
+        container.terminate();
+    }
+
     private void registerHttpServlet() {
         // Register the rack handler
-        final IRubyObject rackHandler = pluginInstance.callMethod("rack_handler");
+        final IRubyObject rackHandler = pluginInstance.callMethod(RACK_HANDLER_RUBY_METHOD_NAME);
         if (!rackHandler.isNil()) {
             logger.log(LogService.LOG_INFO, String.format("Using %s as rack handler", rackHandler.getMetaClass()));
 
@@ -134,19 +135,19 @@ public abstract class JRubyPlugin {
         }
     }
 
-    protected void checkPluginIsRunning() {
+    private void checkPluginIsRunning() {
         if (pluginInstance == null || !(Boolean) pluginInstance.callMethod("is_active").toJava(Boolean.class)) {
             throw new IllegalStateException(String.format("Plugin %s didn't start properly", pluginMainClass));
         }
     }
 
-    protected void checkPluginIsStopped() {
+    private void checkPluginIsStopped() {
         if (pluginInstance == null || (Boolean) pluginInstance.callMethod("is_active").toJava(Boolean.class)) {
             throw new IllegalStateException(String.format("Plugin %s didn't stop properly", pluginMainClass));
         }
     }
 
-    protected void checkValidPlugin() {
+    private void checkValidPlugin() {
         try {
             container.runScriptlet(checkInstanceOfPlugin(KILLBILL_PLUGIN_BASE));
         } catch (EvalFailedException e) {
@@ -154,7 +155,7 @@ public abstract class JRubyPlugin {
         }
     }
 
-    protected void checkValidNotificationPlugin() throws IllegalArgumentException {
+    private void checkValidNotificationPlugin() throws IllegalArgumentException {
         try {
             container.runScriptlet(checkInstanceOfPlugin(KILLBILL_PLUGIN_NOTIFICATION));
         } catch (EvalFailedException e) {
@@ -162,7 +163,7 @@ public abstract class JRubyPlugin {
         }
     }
 
-    protected void checkValidPaymentPlugin() throws IllegalArgumentException {
+    private void checkValidPaymentPlugin() throws IllegalArgumentException {
         try {
             container.runScriptlet(checkInstanceOfPlugin(KILLBILL_PLUGIN_PAYMENT));
         } catch (EvalFailedException e) {
@@ -170,7 +171,7 @@ public abstract class JRubyPlugin {
         }
     }
 
-    protected String checkInstanceOfPlugin(final String baseClass) {
+    private String checkInstanceOfPlugin(final String baseClass) {
         final StringBuilder builder = new StringBuilder(getRequireLine());
         builder.append("raise ArgumentError.new('Invalid plugin: ")
                .append(pluginMainClass)
@@ -220,10 +221,21 @@ public abstract class JRubyPlugin {
         return cachedRequireLine;
     }
 
-    protected Ruby getRuntime() {
+    private Ruby getRuntime() {
         return pluginInstance.getMetaClass().getRuntime();
     }
 
+    private ScriptingContainer setupScriptingContainer() {
+        // SINGLETHREAD model to avoid sharing state across scripting containers
+        // All calls are synchronized anyways (don't trust gems to be thread safe)
+        final ScriptingContainer scriptingContainer = new ScriptingContainer(LocalContextScope.SINGLETHREAD, LocalVariableBehavior.TRANSIENT, true);
+
+        // Set the load paths instead of adding, to avoid looking at the filesystem
+        scriptingContainer.setLoadPaths(Collections.<String>singletonList(pluginLibdir));
+
+        return scriptingContainer;
+    }
+
     public enum VALIDATION_PLUGIN_TYPE {
         NOTIFICATION,
         PAYMENT,
@@ -246,25 +258,27 @@ public abstract class JRubyPlugin {
     }
 
     protected <T> T callWithRuntimeAndChecking(final PluginCallback cb) throws PaymentPluginApiException {
-        try {
-            checkPluginIsRunning();
-
-            switch(cb.getPluginType()) {
-                case NOTIFICATION:
-                    checkValidNotificationPlugin();
-                    break;
-                case PAYMENT:
-                    checkValidPaymentPlugin();
-                    break;
-                default:
-                    break;
+        synchronized (pluginMonitor) {
+            try {
+                checkPluginIsRunning();
+
+                switch (cb.getPluginType()) {
+                    case NOTIFICATION:
+                        checkValidNotificationPlugin();
+                        break;
+                    case PAYMENT:
+                        checkValidPaymentPlugin();
+                        break;
+                    default:
+                        break;
+                }
+
+                final Ruby runtime = getRuntime();
+                return cb.doCall(runtime);
+            } catch (RuntimeException e) {
+                log.warn("RuntimeException in jruby plugin ", e);
+                throw e;
             }
-
-            final Ruby runtime = getRuntime();
-            return cb.doCall(runtime);
-        } catch (RuntimeException e) {
-            log.warn("RuntimeException in jruby plugin ", e);
-            throw e;
         }
     }
 }
diff --git a/osgi-bundles/bundles/logger/pom.xml b/osgi-bundles/bundles/logger/pom.xml
index 746f603..ee33acc 100644
--- a/osgi-bundles/bundles/logger/pom.xml
+++ b/osgi-bundles/bundles/logger/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-bundles-logger</artifactId>
diff --git a/osgi-bundles/bundles/meter/pom.xml b/osgi-bundles/bundles/meter/pom.xml
index c8d6e3e..1c38337 100644
--- a/osgi-bundles/bundles/meter/pom.xml
+++ b/osgi-bundles/bundles/meter/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-bundles-meter</artifactId>
diff --git a/osgi-bundles/bundles/pom.xml b/osgi-bundles/bundles/pom.xml
index a77c6ae..08c60bf 100644
--- a/osgi-bundles/bundles/pom.xml
+++ b/osgi-bundles/bundles/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-all-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-bundles</artifactId>
diff --git a/osgi-bundles/bundles/webconsolebranding/pom.xml b/osgi-bundles/bundles/webconsolebranding/pom.xml
index c97d527..4e56805 100644
--- a/osgi-bundles/bundles/webconsolebranding/pom.xml
+++ b/osgi-bundles/bundles/webconsolebranding/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-bundles-webconsolebranding</artifactId>
diff --git a/osgi-bundles/defaultbundles/pom.xml b/osgi-bundles/defaultbundles/pom.xml
index 4ea9500..f27fab7 100644
--- a/osgi-bundles/defaultbundles/pom.xml
+++ b/osgi-bundles/defaultbundles/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-all-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-bundles-defaultbundles</artifactId>
diff --git a/osgi-bundles/libs/killbill/pom.xml b/osgi-bundles/libs/killbill/pom.xml
index 990fb10..f21d902 100644
--- a/osgi-bundles/libs/killbill/pom.xml
+++ b/osgi-bundles/libs/killbill/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-lib-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-bundles-lib-killbill</artifactId>
diff --git a/osgi-bundles/libs/pom.xml b/osgi-bundles/libs/pom.xml
index 53e8e0a..996560a 100644
--- a/osgi-bundles/libs/pom.xml
+++ b/osgi-bundles/libs/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-all-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-lib-bundles</artifactId>
diff --git a/osgi-bundles/libs/slf4j-osgi/pom.xml b/osgi-bundles/libs/slf4j-osgi/pom.xml
index da5abbe..8eaa978 100644
--- a/osgi-bundles/libs/slf4j-osgi/pom.xml
+++ b/osgi-bundles/libs/slf4j-osgi/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-lib-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-bundles-lib-slf4j-osgi</artifactId>
diff --git a/osgi-bundles/pom.xml b/osgi-bundles/pom.xml
index 3eb80f3..d126189 100644
--- a/osgi-bundles/pom.xml
+++ b/osgi-bundles/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-all-bundles</artifactId>
diff --git a/osgi-bundles/tests/beatrix/pom.xml b/osgi-bundles/tests/beatrix/pom.xml
index c02804c..0cbe690 100644
--- a/osgi-bundles/tests/beatrix/pom.xml
+++ b/osgi-bundles/tests/beatrix/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-test-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-bundles-test-beatrix</artifactId>
diff --git a/osgi-bundles/tests/payment/pom.xml b/osgi-bundles/tests/payment/pom.xml
index aa22159..9af18f6 100644
--- a/osgi-bundles/tests/payment/pom.xml
+++ b/osgi-bundles/tests/payment/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-test-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-bundles-test-payment</artifactId>
diff --git a/osgi-bundles/tests/pom.xml b/osgi-bundles/tests/pom.xml
index a35df0b..8b94366 100644
--- a/osgi-bundles/tests/pom.xml
+++ b/osgi-bundles/tests/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill-osgi-all-bundles</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-osgi-test-bundles</artifactId>

overdue/pom.xml 2(+1 -1)

diff --git a/overdue/pom.xml b/overdue/pom.xml
index 65e44a4..e0a7710 100644
--- a/overdue/pom.xml
+++ b/overdue/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-overdue</artifactId>

payment/pom.xml 2(+1 -1)

diff --git a/payment/pom.xml b/payment/pom.xml
index cb11d1d..07dddfc 100644
--- a/payment/pom.xml
+++ b/payment/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-payment</artifactId>

pom.xml 11(+8 -3)

diff --git a/pom.xml b/pom.xml
index 6d819af..0a2927f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
     <groupId>com.ning.billing</groupId>
     <artifactId>killbill</artifactId>
     <packaging>pom</packaging>
-    <version>0.1.77-SNAPSHOT</version>
+    <version>0.1.78-SNAPSHOT</version>
     <name>killbill</name>
     <description>Library for managing recurring subscriptions and the associated billing</description>
     <url>http://github.com/killbill/killbill</url>
@@ -42,8 +42,8 @@
     </scm>
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <killbill-commons.version>0.1.3</killbill-commons.version>
-        <slf4j.version>1.7.2</slf4j.version>
+        <killbill-commons.version>0.1.5</killbill-commons.version>
+        <slf4j.version>1.7.5</slf4j.version>
         <ehcache.version>2.6.2</ehcache.version>
     </properties>
     <modules>
@@ -316,6 +316,11 @@
             </dependency>
             <dependency>
                 <groupId>com.ning.billing.commons</groupId>
+                <artifactId>killbill-concurrent</artifactId>
+                <version>${killbill-commons.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.ning.billing.commons</groupId>
                 <artifactId>killbill-embeddeddb</artifactId>
                 <version>${killbill-commons.version}</version>
             </dependency>

server/pom.xml 2(+1 -1)

diff --git a/server/pom.xml b/server/pom.xml
index 91b3cc1..9b367c2 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-server</artifactId>

tenant/pom.xml 2(+1 -1)

diff --git a/tenant/pom.xml b/tenant/pom.xml
index b6a6552..42ef63d 100644
--- a/tenant/pom.xml
+++ b/tenant/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-tenant</artifactId>

usage/pom.xml 2(+1 -1)

diff --git a/usage/pom.xml b/usage/pom.xml
index c3a90eb..0c550e8 100644
--- a/usage/pom.xml
+++ b/usage/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-usage</artifactId>

util/pom.xml 2(+1 -1)

diff --git a/util/pom.xml b/util/pom.xml
index f070310..c65c0c0 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.1.77-SNAPSHOT</version>
+        <version>0.1.78-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-util</artifactId>