Java tutorial
/* * Copyright 2010-2014 Ning, Inc. * 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.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.UUID; import javax.annotation.Nullable; import org.joda.time.LocalDate; import org.killbill.billing.invoice.api.InvoiceItem; import org.killbill.billing.invoice.tree.Item.ItemAction; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; /** * Keeps track of all the items existing on a specified ItemsNodeInterval */ public class ItemsInterval { // Parent (enclosing) interval private final ItemsNodeInterval interval; private final LinkedList<Item> items; public ItemsInterval(final ItemsNodeInterval interval) { this(interval, null); } public ItemsInterval(final ItemsNodeInterval interval, final Item initialItem) { this.interval = interval; this.items = Lists.newLinkedList(); if (initialItem != null) { items.add(initialItem); } } public List<Item> getItems() { return items; } public Iterable<Item> get_ADD_items() { return findItems(ItemAction.ADD); } public Iterable<Item> get_CANCEL_items() { return findItems(ItemAction.CANCEL); } public Item getCancellingItemIfExists(final UUID targetId) { return Iterables.tryFind(items, new Predicate<Item>() { @Override public boolean apply(final Item input) { return input.getAction() == ItemAction.CANCEL && input.getLinkedId().equals(targetId); } }).orNull(); } public Item getCancelledItemIfExists(final UUID linkedId) { return Iterables.tryFind(items, new Predicate<Item>() { @Override public boolean apply(final Item input) { return input.getAction() == ItemAction.ADD && input.getId().equals(linkedId); } }).orNull(); } public NodeInterval getNodeInterval() { return interval; } public Item findItem(final UUID targetId) { final Collection<Item> matchingItems = Collections2.<Item>filter(items, new Predicate<Item>() { @Override public boolean apply(final Item input) { return input.getId().equals(targetId); } }); Preconditions.checkState(matchingItems.size() < 2, "Too many items matching id='%s' among items='%s'", targetId, items); return matchingItems.size() == 1 ? matchingItems.iterator().next() : null; } /** * Remove all the cancelling pairs (ADD/CANCEL) for which CANCEL linkedId points to ADD id. * * @return true if there is no more items */ public boolean mergeCancellingPairs() { final Multimap<UUID, Item> cancellingPairPerInvoiceItemId = LinkedListMultimap.<UUID, Item>create(); for (final Item item : items) { final UUID invoiceItemId = (item.getAction() == ItemAction.ADD) ? item.getId() : item.getLinkedId(); cancellingPairPerInvoiceItemId.put(invoiceItemId, item); } for (final UUID invoiceItemId : cancellingPairPerInvoiceItemId.keySet()) { final Collection<Item> itemsToRemove = cancellingPairPerInvoiceItemId.get(invoiceItemId); Preconditions.checkState(itemsToRemove.size() <= 2, "Too many repairs for invoiceItemId='%s': %s", invoiceItemId, itemsToRemove); if (itemsToRemove.size() == 2) { for (final Item itemToRemove : itemsToRemove) { items.remove(itemToRemove); } } } return items.isEmpty(); } public void add(final Item item) { items.add(item); } public void cancelItems(final Item item) { Preconditions.checkState(item.getAction() == ItemAction.ADD); Preconditions.checkState(items.size() == 1); Preconditions.checkState(items.get(0).getAction() == ItemAction.CANCEL); items.clear(); } public void remove(final Item item) { items.remove(item); } // Called for missing service periods public void buildForMissingInterval(@Nullable final LocalDate startDate, @Nullable final LocalDate endDate, @Nullable final UUID targetInvoiceId, final Collection<Item> output, final boolean addRepair) { final Item item = createNewItem(startDate, endDate, targetInvoiceId, addRepair); if (item != null) { output.add(item); } } // Called on the last node public void buildFromItems(final Collection<Item> output, final boolean mergeMode) { buildForMissingInterval(null, null, null, output, mergeMode); } /** * Create a new item based on the existing items and new service period * <p/> * <ul> * <li>During the build phase, we only consider ADD items. This happens when for instance an existing item was partially repaired * and there is a need to create a new item which represents the part left -- that was not repaired. * <li>During the merge phase, we create new items that are the missing repaired items (CANCEL). * </ul> * * @param startDate start date of the new item to create * @param endDate end date of the new item to create * @param mergeMode mode to consider. * @return new item for this service period or null */ private Item createNewItem(@Nullable final LocalDate startDate, @Nullable final LocalDate endDate, @Nullable final UUID targetInvoiceId, final boolean mergeMode) { // Find the ADD (build phase) or CANCEL (merge phase) item of this interval final Item item = getResultingItem(mergeMode); if (item == null || startDate == null || endDate == null || targetInvoiceId == null) { return item; } // Prorate (build phase) or repair (merge phase) this item, as needed final InvoiceItem proratedInvoiceItem = item.toProratedInvoiceItem(startDate, endDate); if (proratedInvoiceItem == null) { return null; } else { // Keep track of the repaired amount for this item item.incrementCurrentRepairedAmount(proratedInvoiceItem.getAmount().abs()); return new Item(proratedInvoiceItem, targetInvoiceId, item.getAction()); } } private Item getResultingItem(final boolean mergeMode) { return mergeMode ? getResulting_CANCEL_Item() : getResulting_ADD_Item(); } private Item getResulting_CANCEL_Item() { Preconditions.checkState(items.size() <= 1, "Too many items=%s", items); return getResulting_CANCEL_ItemNoChecks(); } private Item getResulting_CANCEL_ItemNoChecks() { return findItem(ItemAction.CANCEL); } private Item getResulting_ADD_Item() { // // At this point we pruned the items so that we can have either: // - 2 items (ADD + CANCEL, where CANCEL does NOT point to ADD item -- otherwise this is a cancelling pair that // would have been removed in mergeCancellingPairs logic) // - 1 ADD item, simple enough we return it // - 1 CANCEL, there is nothing to return but the period will be ignored by the parent // - Nothing at all; this valid, this just means its original items got removed during mergeCancellingPairs logic, // but its NodeInterval has children so it could not be deleted. // Preconditions.checkState(items.size() <= 2, "Double billing detected: %s", items); final Collection<Item> addItems = findItems(ItemAction.ADD); Preconditions.checkState(addItems.size() <= 1, "Double billing detected: %s", items); final Item item = findItem(ItemAction.ADD); // Double billing sanity check across nodes if (item != null) { final Set<UUID> addItemsCancelled = new HashSet<UUID>(); final Item cancelItem = findItem(ItemAction.CANCEL); if (cancelItem != null) { Preconditions.checkState(cancelItem.getLinkedId() != null, "Invalid CANCEL item=%s", cancelItem); addItemsCancelled.add(cancelItem.getLinkedId()); } final Set<UUID> addItemsToBeCancelled = new HashSet<UUID>(); checkDoubleBilling(addItemsCancelled, addItemsToBeCancelled); } return item; } private void checkDoubleBilling(final Set<UUID> addItemsCancelled, final Set<UUID> addItemsToBeCancelled) { final ItemsNodeInterval parentNodeInterval = (ItemsNodeInterval) interval.getParent(); if (parentNodeInterval == null) { Preconditions.checkState(addItemsCancelled.equals(addItemsToBeCancelled), "Double billing detected: addItemsCancelled=%s, addItemsToBeCancelled=%s", addItemsCancelled, addItemsToBeCancelled); return; } final ItemsInterval parentItemsInterval = parentNodeInterval.getItemsInterval(); final Item parentAddItem = parentItemsInterval.getResulting_ADD_Item(); if (parentAddItem != null) { Preconditions.checkState(parentAddItem.getId() != null, "Invalid ADD item=%s", parentAddItem); addItemsToBeCancelled.add(parentAddItem.getId()); } final Item parentCancelItem = parentItemsInterval.getResulting_CANCEL_ItemNoChecks(); if (parentCancelItem != null) { Preconditions.checkState(parentCancelItem.getLinkedId() != null, "Invalid CANCEL item=%s", parentCancelItem); addItemsCancelled.add(parentCancelItem.getLinkedId()); } parentItemsInterval.checkDoubleBilling(addItemsCancelled, addItemsToBeCancelled); } private Item findItem(final ItemAction itemAction) { final Collection<Item> matchingItems = findItems(itemAction); return matchingItems.size() == 1 ? matchingItems.iterator().next() : null; } private Collection<Item> findItems(final ItemAction itemAction) { return Collections2.<Item>filter(items, new Predicate<Item>() { @Override public boolean apply(final Item input) { return input.getAction() == itemAction; } }); } @Override public String toString() { final StringBuilder sb = new StringBuilder("ItemsInterval{"); sb.append("items=").append(items); sb.append('}'); return sb.toString(); } }