killbill-memoizeit

analytics: implement filtering Signed-off-by: Pierre-Alexandre

5/17/2013 8:54:28 PM

Details

diff --git a/osgi-bundles/bundles/analytics/README.md b/osgi-bundles/bundles/analytics/README.md
index b411bec..41847d6 100644
--- a/osgi-bundles/bundles/analytics/README.md
+++ b/osgi-bundles/bundles/analytics/README.md
@@ -44,3 +44,4 @@ The dashboard system is controlled by query parameters:
  * 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/http/AnalyticsServlet.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java
index 6fbcf1a..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
@@ -173,8 +173,8 @@ public class AnalyticsServlet extends HttpServlet {
     }
 
     private void doHandleReports(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
-        final String[] reportNames = req.getParameterValues(REPORTS_QUERY_NAME);
-        if (reportNames == null || reportNames.length == 0) {
+        final String[] rawReportNames = req.getParameterValues(REPORTS_QUERY_NAME);
+        if (rawReportNames == null || rawReportNames.length == 0) {
             resp.sendError(404);
             return;
         }
@@ -185,7 +185,7 @@ public class AnalyticsServlet extends HttpServlet {
         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(reportNames, startDate, endDate, smootherType);
+        final List<NamedXYTimeSeries> result = reportsUserApi.getTimeSeriesDataForReport(rawReportNames, startDate, endDate, smootherType);
 
         resp.getOutputStream().write(mapper.writeValueAsBytes(result));
         resp.setContentType("application/json");
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
index cc0b89c..1dbd9b3 100644
--- 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
@@ -16,6 +16,7 @@
 
 package com.ning.billing.osgi.bundles.analytics.reports;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.LinkedHashMap;
@@ -67,18 +68,24 @@ public class ReportsUserApi {
         dbiThreadsExecutor.shutdownNow();
     }
 
-    public List<NamedXYTimeSeries> getTimeSeriesDataForReport(final String[] reportNames,
+    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(reportNames, dataForReports);
+        fetchData(reportSpecifications, dataForReports);
 
         // Filter the data first
-        filterValues(dataForReports, startDate, endDate);
+        filterValues(reportSpecifications, dataForReports, startDate, endDate);
 
         // Normalize and sort the data
         normalizeAndSortXValues(dataForReports, startDate, endDate);
@@ -113,9 +120,10 @@ public class ReportsUserApi {
         return results;
     }
 
-    private void fetchData(final String[] reportNames, final Map<String, Map<String, List<XY>>> dataForReports) {
+    private void fetchData(final List<ReportSpecification> reportSpecifications, final Map<String, Map<String, List<XY>>> dataForReports) {
         final List<Future> jobs = new LinkedList<Future>();
-        for (final String reportName : reportNames) {
+        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() {
@@ -139,12 +147,36 @@ public class ReportsUserApi {
         }
     }
 
-    private void filterValues(final Map<String, Map<String, List<XY>>> dataForReports, @Nullable final LocalDate startDate, @Nullable final LocalDate endDate) {
+    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 Map<String, List<XY>> dataForReport : dataForReports.values()) {
+        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>() {