killbill-memoizeit
Changes
account/pom.xml 2(+1 -1)
analytics/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)
entitlement/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)
osgi-bundles/bundles/analytics/pom.xml 11(+10 -1)
osgi-bundles/bundles/analytics/README.md 47(+47 -0)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsActivator.java 25(+23 -2)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/BusinessExecutor.java 154(+18 -136)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java 34(+33 -1)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/json/XY.java 33(+30 -3)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/AverageSmoother.java 34(+34 -0)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/DateGranularity.java 22(+22 -0)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/Smoother.java 134(+134 -0)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/SummingSmoother.java 34(+34 -0)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportConfigurationSection.java 36(+36 -0)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsConfiguration.java 94(+94 -0)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportSpecification.java 84(+84 -0)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsUserApi.java 300(+300 -0)
osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/scheduler/JobsScheduler.java 129(+129 -0)
osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_adjustments_per_day.sql 10(+10 -0)
osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_item_adjustments_per_day.sql 10(+10 -0)
osgi-bundles/bundles/analytics/src/main/resources/reports/invoice_item_credits_per_day.sql 10(+10 -0)
osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_notifications_per_queue_name.sql 10(+10 -0)
osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_payment_failure.sql 11(+11 -0)
osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_payment_failure_aborted.sql 11(+11 -0)
osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_plugin_failure.sql 11(+11 -0)
osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_plugin_failure_aborted.sql 11(+11 -0)
osgi-bundles/bundles/analytics/src/main/resources/reports/system_report_payment_success.sql 11(+11 -0)
osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/api/user/TestDefaultAnalyticsUserApi.java 2(+1 -1)
osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/factory/TestBusinessBundleSummaryFactory.java 2(+1 -1)
osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/factory/TestBusinessInvoiceFactory.java 2(+1 -1)
osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/http/TestAnalyticsServlet.java 2(+1 -1)
osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/TestBusinessExecutor.java 3(+2 -1)
osgi-bundles/bundles/jruby/pom.xml 6(+5 -1)
osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyActivator.java 110(+81 -29)
osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java 14(+2 -12)
osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java 11(+5 -6)
osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java 108(+61 -47)
osgi-bundles/bundles/logger/pom.xml 2(+1 -1)
osgi-bundles/bundles/meter/pom.xml 2(+1 -1)
osgi-bundles/bundles/pom.xml 2(+1 -1)
osgi-bundles/defaultbundles/pom.xml 2(+1 -1)
osgi-bundles/libs/killbill/pom.xml 2(+1 -1)
osgi-bundles/libs/pom.xml 2(+1 -1)
osgi-bundles/libs/slf4j-osgi/pom.xml 2(+1 -1)
osgi-bundles/pom.xml 2(+1 -1)
osgi-bundles/tests/beatrix/pom.xml 2(+1 -1)
osgi-bundles/tests/payment/pom.xml 2(+1 -1)
osgi-bundles/tests/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>
analytics/pom.xml 2(+1 -1)
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>
entitlement/pom.xml 2(+1 -1)
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;
osgi-bundles/bundles/analytics/pom.xml 11(+10 -1)
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>
osgi-bundles/bundles/analytics/README.md 47(+47 -0)
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;
osgi-bundles/bundles/jruby/pom.xml 6(+5 -1)
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;
}
}
}
osgi-bundles/bundles/logger/pom.xml 2(+1 -1)
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>
osgi-bundles/bundles/meter/pom.xml 2(+1 -1)
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>
osgi-bundles/bundles/pom.xml 2(+1 -1)
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>
osgi-bundles/defaultbundles/pom.xml 2(+1 -1)
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>
osgi-bundles/libs/killbill/pom.xml 2(+1 -1)
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>
osgi-bundles/libs/pom.xml 2(+1 -1)
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>
osgi-bundles/libs/slf4j-osgi/pom.xml 2(+1 -1)
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>
osgi-bundles/pom.xml 2(+1 -1)
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>
osgi-bundles/tests/beatrix/pom.xml 2(+1 -1)
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>
osgi-bundles/tests/payment/pom.xml 2(+1 -1)
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>
osgi-bundles/tests/pom.xml 2(+1 -1)
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>