killbill-aplcache

invoice: add tree pretty printer Some examples: A / B / C--d /

12/27/2016 1:16:38 PM

Details

diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/TreePrinter.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/TreePrinter.java
new file mode 100644
index 0000000..72a5bdd
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/TreePrinter.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.invoice.tree;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class TreePrinter {
+
+    public static String print(final ItemsNodeInterval root) {
+        return print(buildCoordinates(root));
+    }
+
+    private static String print(final SortedMap<XY, ItemsNodeInterval> tree) {
+        // Make left most node start at X=0
+        translate(tree);
+
+        final AtomicInteger totalOrdering = new AtomicInteger(64);
+        final Map<String, ItemsNodeInterval> legend = new LinkedHashMap<String, ItemsNodeInterval>();
+
+        final List<StringBuilder> builders = new LinkedList<StringBuilder>();
+        for (int level = 0; level >= maxOffset(tree).Y; level--) {
+            builders.add(new StringBuilder());
+            // Draw edges for that level
+            drawLevel(true, level, tree, builders, totalOrdering, legend);
+
+            // Draw nodes for that level
+            builders.add(new StringBuilder());
+            drawLevel(false, level, tree, builders, totalOrdering, legend);
+        }
+
+        final StringBuilder builder = new StringBuilder();
+        for (final StringBuilder levelBuilder : builders) {
+            builder.append(levelBuilder.toString());
+        }
+
+        builder.append("\n");
+        for (final String key : legend.keySet()) {
+            builder.append(key).append(": ");
+            appendNodeDetails(legend.get(key), builder);
+            builder.append("\n");
+        }
+        return builder.toString();
+    }
+
+    private static void drawLevel(final boolean drawEdges,
+                                  final int level,
+                                  final SortedMap<XY, ItemsNodeInterval> tree,
+                                  final List<StringBuilder> builders,
+                                  final AtomicInteger totalOrdering,
+                                  final Map<String, ItemsNodeInterval> legend) {
+        if (drawEdges && level == 0) {
+            // Nothing to do for root
+            return;
+        }
+
+        final StringBuilder builder = builders.get(builders.size() - 1);
+
+        int posX = 0;
+        boolean sibling;
+        for (final XY levelXY : tree.keySet()) {
+            if (levelXY.Y > level) {
+                // Sorted - we haven't reached that level yet
+                continue;
+            } else if (levelXY.Y < level) {
+                // Sorted - we are done for that level
+                break;
+            }
+
+            sibling = levelXY.parent == null;
+            while (posX < levelXY.X) {
+                if (drawEdges || !sibling || level == 0) {
+                    builder.append(" ");
+                } else {
+                    // Link siblings
+                    builder.append("-");
+                }
+                posX++;
+            }
+
+            if (drawEdges) {
+                if (sibling) {
+                    builder.append(" ");
+                } else {
+                    builder.append("/");
+                }
+            } else {
+                if (sibling && level != 0) {
+                    builder.append("");
+                }
+
+                // Print this node
+                String node = Character.toString((char) totalOrdering.incrementAndGet());
+                if (sibling && level != 0) {
+                    node = node.toLowerCase();
+                }
+                legend.put(node, tree.get(levelXY));
+                builder.append(node);
+            }
+            posX++;
+        }
+        builder.append("\n");
+    }
+
+    private static void appendNodeDetails(final ItemsNodeInterval interval, final StringBuilder builder) {
+        builder.append("[")
+               .append(interval.getStart())
+               .append(",")
+               .append(interval.getEnd())
+               .append("]");
+
+        if (interval.getItems().isEmpty()) {
+            return;
+        }
+
+        builder.append("(");
+        final List<Item> items = interval.getItems();
+        for (int i = 0; i < items.size(); i++) {
+            final Item item = items.get(i);
+            if (i > 0) {
+                builder.append(",");
+            }
+            builder.append(item.getAction().name().charAt(0));
+        }
+        builder.append(")");
+    }
+
+    public static SortedMap<XY, ItemsNodeInterval> buildCoordinates(final ItemsNodeInterval root) {
+        final XY reference = new XY(0, 0);
+
+        final SortedMap<XY, ItemsNodeInterval> result = new TreeMap<XY, ItemsNodeInterval>();
+        result.put(reference, root);
+        result.putAll(buildCoordinates(root, reference));
+
+        return result;
+    }
+
+    public static Map<XY, ItemsNodeInterval> buildCoordinates(final ItemsNodeInterval root, final XY initialCoords) {
+        final Map<XY, ItemsNodeInterval> result = new HashMap<XY, ItemsNodeInterval>();
+        if (root == null) {
+            return result;
+        }
+
+        // Compute the coordinate of the left most child
+        ItemsNodeInterval curChild = (ItemsNodeInterval) root.getLeftChild();
+        if (curChild == null) {
+            return result;
+        }
+
+        XY curXY = leftChildXY(initialCoords);
+        result.put(curXY, curChild);
+        // Compute the coordinates of the tree below that child
+        result.putAll(buildCoordinates(curChild, curXY));
+
+        curChild = (ItemsNodeInterval) curChild.getRightSibling();
+        while (curChild != null) {
+            curXY = rightSiblingXY(curXY);
+
+            // Compute the coordinates of the tree below that child
+            final Map<XY, ItemsNodeInterval> subtree = buildCoordinates(curChild, curXY);
+            final XY offset = translate(subtree);
+            translate(offset, curXY);
+            result.put(curXY, curChild);
+            result.putAll(subtree);
+
+            curChild = (ItemsNodeInterval) curChild.getRightSibling();
+        }
+
+        return result;
+    }
+
+    private static XY translate(final Map<XY, ItemsNodeInterval> subtree) {
+        final XY offset = maxOffset(subtree);
+        for (final XY xy : subtree.keySet()) {
+            translate(offset, xy);
+        }
+        return offset;
+    }
+
+    private static void translate(final XY offset, final XY xy) {
+        xy.X = xy.X - offset.X;
+    }
+
+    private static XY maxOffset(final Map<XY, ItemsNodeInterval> tree) {
+        final XY res = new XY(0, 0);
+        for (final XY xy : tree.keySet()) {
+            if (xy.X < res.X) {
+                res.X = xy.X;
+            }
+            if (xy.Y < res.Y) {
+                res.Y = xy.Y;
+            }
+        }
+        return res;
+    }
+
+    private static XY leftChildXY(final XY parent) {
+        return new XY(parent.X - 1, parent.Y - 1, parent);
+    }
+
+    private static XY rightSiblingXY(final XY leftSibling) {
+        return new XY(leftSibling.X + 1, leftSibling.Y);
+    }
+
+    static class XY implements Comparable<XY> {
+
+        int X;
+        int Y;
+        XY parent;
+
+        public XY(final int x, final int y) {
+            this(x, y, null);
+        }
+
+        public XY(final int x, final int y, final XY parent) {
+            X = x;
+            Y = y;
+            this.parent = parent;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder("(");
+            sb.append(X);
+            sb.append(",").append(Y);
+            sb.append(')');
+            return sb.toString();
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            final XY xy = (XY) o;
+
+            if (X != xy.X) {
+                return false;
+            }
+            return Y == xy.Y;
+
+        }
+
+        @Override
+        public int hashCode() {
+            int result = X;
+            result = 31 * result + Y;
+            return result;
+        }
+
+        @Override
+        public int compareTo(final XY o) {
+            return Y == o.Y ? X < o.X ? -1 : X == o.X ? 0 : 1 : Y < o.Y ? 1 : -1;
+        }
+    }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
index 9f592b3..26f4170 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -63,6 +63,58 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
     private final String phaseName = "my-phase";
     private final Currency currency = Currency.USD;
 
+    @Test(groups = "fast", description = "Complex multi-level tree, mostly used to test the tree printer")
+    public void testMultipleLevels() throws Exception {
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final LocalDate startRepairDate1 = new LocalDate(2014, 1, 10);
+        final LocalDate endRepairDate1 = new LocalDate(2014, 1, 15);
+
+        final LocalDate startRepairDate11 = new LocalDate(2014, 1, 10);
+        final LocalDate endRepairDate12 = new LocalDate(2014, 1, 12);
+
+        final LocalDate startRepairDate2 = new LocalDate(2014, 1, 20);
+        final LocalDate endRepairDate2 = new LocalDate(2014, 1, 25);
+
+        final LocalDate startRepairDate21 = new LocalDate(2014, 1, 22);
+        final LocalDate endRepairDate22 = new LocalDate(2014, 1, 23);
+
+        final BigDecimal rate = BigDecimal.TEN;
+        final BigDecimal amount = rate;
+
+        final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, rate, currency);
+
+        final InvoiceItem newItem1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepairDate1, endRepairDate1, amount, rate, currency);
+        final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, startRepairDate1, endRepairDate1, amount.negate(), currency, initial.getId());
+
+        final InvoiceItem newItem11 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepairDate11, endRepairDate12, amount, rate, currency);
+        final InvoiceItem repair12 = new RepairAdjInvoiceItem(invoiceId, accountId, startRepairDate11, endRepairDate12, amount.negate(), currency, newItem1.getId());
+
+        final InvoiceItem newItem2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepairDate2, endRepairDate2, amount, rate, currency);
+        final InvoiceItem repair2 = new RepairAdjInvoiceItem(invoiceId, accountId, startRepairDate2, endRepairDate2, amount.negate(), currency, initial.getId());
+
+        final InvoiceItem newItem21 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepairDate21, endRepairDate22, amount, rate, currency);
+        final InvoiceItem repair22 = new RepairAdjInvoiceItem(invoiceId, accountId, startRepairDate21, endRepairDate22, amount.negate(), currency, newItem2.getId());
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+        tree.addItem(initial);
+        tree.addItem(newItem1);
+        tree.addItem(repair1);
+        tree.addItem(newItem11);
+        tree.addItem(repair12);
+        tree.addItem(newItem2);
+        tree.addItem(repair2);
+        tree.addItem(newItem21);
+        tree.addItem(repair22);
+
+        tree.build();
+        //printTree(tree);
+
+        tree.flatten(true);
+        //printTree(tree);
+    }
+
     @Test(groups = "fast")
     public void testWithExistingSplitRecurring() {
 
@@ -1441,9 +1493,13 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
         Assert.assertEquals(previousExistingSize, 3);
     }
 
-    private void printTree(final SubscriptionItemTree tree) throws IOException {
+    private void printTreeJSON(final SubscriptionItemTree tree) throws IOException {
         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
         tree.getRoot().jsonSerializeTree(OBJECT_MAPPER, outputStream);
         System.out.println(outputStream.toString("UTF-8"));
     }
+
+    private void printTree(final SubscriptionItemTree tree) throws IOException {
+        System.out.println(TreePrinter.print(tree.getRoot()));
+    }
 }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestTreePrinter.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestTreePrinter.java
new file mode 100644
index 0000000..6159bdc
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestTreePrinter.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.invoice.tree;
+
+import java.math.BigDecimal;
+import java.util.Map;
+import java.util.SortedMap;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.tree.Item.ItemAction;
+import org.killbill.billing.invoice.tree.TreePrinter.XY;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class TestTreePrinter extends InvoiceTestSuiteNoDB {
+
+    private ItemsNodeInterval root;
+    private ItemsNodeInterval node11;
+    private ItemsNodeInterval node21;
+    private ItemsNodeInterval node22;
+    private ItemsNodeInterval node12;
+    private ItemsNodeInterval node23;
+    private ItemsNodeInterval node31;
+
+    @BeforeMethod(groups = "fast")
+    public void setUp() throws Exception {
+        final InvoiceItem item = Mockito.mock(InvoiceItem.class);
+        Mockito.when(item.getAmount()).thenReturn(BigDecimal.ZERO);
+
+        root = new ItemsNodeInterval(null, new Item(item, new LocalDate(2016, 1, 1), new LocalDate(2016, 2, 1), null, ItemAction.ADD));
+
+        node11 = new ItemsNodeInterval(root, new Item(item, new LocalDate(2016, 1, 10), new LocalDate(2016, 1, 15), null, ItemAction.ADD));
+        node21 = new ItemsNodeInterval(node11, new Item(item, new LocalDate(2016, 1, 10), new LocalDate(2016, 1, 12), null, ItemAction.ADD));
+        node22 = new ItemsNodeInterval(node11, new Item(item, new LocalDate(2016, 1, 14), new LocalDate(2016, 1, 15), null, ItemAction.ADD));
+
+        node12 = new ItemsNodeInterval(root, new Item(item, new LocalDate(2016, 1, 20), new LocalDate(2016, 1, 25), null, ItemAction.ADD));
+        node23 = new ItemsNodeInterval(node12, new Item(item, new LocalDate(2016, 1, 22), new LocalDate(2016, 1, 24), null, ItemAction.ADD));
+        node31 = new ItemsNodeInterval(node23, new Item(item, new LocalDate(2016, 1, 22), new LocalDate(2016, 1, 23), null, ItemAction.ADD));
+    }
+
+    @Test(groups = "fast")
+    public void testSimpleTranslate() throws Exception {
+        root.leftChild = node11;
+        node11.rightSibling = node12;
+        node12.leftChild = node23;
+
+        final SortedMap<XY, ItemsNodeInterval> coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 4);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(1, -1)), node12);
+        Assert.assertEquals(coords.get(new XY(0, -2)), node23);
+        //System.out.println(TreePrinter.print(root));
+    }
+
+    @Test(groups = "fast")
+    public void testComplexMultiLevelTree() throws Exception {
+        Map<XY, ItemsNodeInterval> coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 1);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+
+        root.leftChild = node11;
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 2);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+
+        node11.rightSibling = node12;
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 3);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(0, -1)), node12);
+
+        node11.leftChild = node21;
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 4);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(0, -1)), node12);
+        Assert.assertEquals(coords.get(new XY(-2, -2)), node21);
+
+        node21.rightSibling = node22;
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 5);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(0, -1)), node12);
+        Assert.assertEquals(coords.get(new XY(-2, -2)), node21);
+        Assert.assertEquals(coords.get(new XY(-1, -2)), node22);
+
+        node12.leftChild = node23;
+        //System.out.println(TreePrinter.print(root));
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 6);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(1, -1)), node12); // (0,-1) before translation
+        Assert.assertEquals(coords.get(new XY(-2, -2)), node21);
+        Assert.assertEquals(coords.get(new XY(-1, -2)), node22);
+        Assert.assertEquals(coords.get(new XY(0, -2)), node23); // (-1,-2) before translation
+
+        node23.leftChild = node31;
+        //System.out.println(TreePrinter.print(root));
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 7);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(2, -1)), node12); // (1,-1) before translation
+        Assert.assertEquals(coords.get(new XY(-2, -2)), node21);
+        Assert.assertEquals(coords.get(new XY(-1, -2)), node22);
+        Assert.assertEquals(coords.get(new XY(1, -2)), node23); // (0,-2) before translation
+        Assert.assertEquals(coords.get(new XY(0, -3)), node31); // (-1,-3) before translation
+    }
+}