Java tutorial
/* * 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 org.killbill.billing.entitlement.engine.core; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import javax.annotation.Nullable; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; import org.killbill.billing.account.api.Account; import org.killbill.billing.callcontext.InternalTenantContext; import org.killbill.billing.catalog.api.Plan; import org.killbill.billing.catalog.api.Product; import org.killbill.billing.catalog.api.ProductCategory; import org.killbill.billing.entitlement.EntitlementService; import org.killbill.billing.entitlement.EventsStream; import org.killbill.billing.entitlement.api.BlockingState; import org.killbill.billing.entitlement.api.BlockingStateType; import org.killbill.billing.entitlement.api.DefaultEntitlementApi; import org.killbill.billing.entitlement.api.Entitlement.EntitlementState; import org.killbill.billing.entitlement.block.BlockingChecker; import org.killbill.billing.entitlement.block.BlockingChecker.BlockingAggregator; import org.killbill.billing.junction.DefaultBlockingState; import org.killbill.billing.subscription.api.SubscriptionBase; import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType; import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle; import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; public class DefaultEventsStream implements EventsStream { private final Account account; private final SubscriptionBaseBundle bundle; // All blocking states for the account, associated bundle or subscription private final List<BlockingState> blockingStates; private final BlockingChecker blockingChecker; // Base subscription for the bundle if it exists, null otherwise private final SubscriptionBase baseSubscription; // Subscription associated with this entitlement (equals to baseSubscription for base subscriptions) private final SubscriptionBase subscription; // All subscriptions for that bundle private final List<SubscriptionBase> allSubscriptionsForBundle; private final InternalTenantContext internalTenantContext; private final DateTime utcNow; private BlockingAggregator blockingAggregator; private List<BlockingState> subscriptionEntitlementStates; private List<BlockingState> bundleEntitlementStates; private List<BlockingState> accountEntitlementStates; private List<BlockingState> currentSubscriptionEntitlementBlockingStatesForServices; private List<BlockingState> currentBundleEntitlementBlockingStatesForServices; private List<BlockingState> currentAccountEntitlementBlockingStatesForServices; private LocalDate entitlementEffectiveEndDate; private BlockingState entitlementCancelEvent; private EntitlementState entitlementState; public DefaultEventsStream(final Account account, final SubscriptionBaseBundle bundle, final List<BlockingState> blockingStates, final BlockingChecker blockingChecker, @Nullable final SubscriptionBase baseSubscription, final SubscriptionBase subscription, final List<SubscriptionBase> allSubscriptionsForBundle, final InternalTenantContext contextWithValidAccountRecordId, final DateTime utcNow) { this.account = account; this.bundle = bundle; this.blockingStates = blockingStates; this.blockingChecker = blockingChecker; this.baseSubscription = baseSubscription; this.subscription = subscription; this.allSubscriptionsForBundle = allSubscriptionsForBundle; this.internalTenantContext = contextWithValidAccountRecordId; this.utcNow = utcNow; setup(); } @Override public DateTimeZone getAccountTimeZone() { return account.getTimeZone(); } @Override public UUID getAccountId() { return account.getId(); } @Override public UUID getBundleId() { return bundle.getId(); } @Override public String getBundleExternalKey() { return bundle.getExternalKey(); } @Override public UUID getEntitlementId() { return subscription.getId(); } @Override public SubscriptionBase getBasePlanSubscriptionBase() { return baseSubscription; } @Override public SubscriptionBase getSubscriptionBase() { return subscription; } @Override public InternalTenantContext getInternalTenantContext() { return internalTenantContext; } @Override public LocalDate getEntitlementEffectiveEndDate() { return entitlementEffectiveEndDate; } @Override public EntitlementState getEntitlementState() { return entitlementState; } @Override public boolean isBlockChange() { return blockingAggregator.isBlockChange(); } @Override public List<BlockingState> getCurrentSubscriptionEntitlementBlockingStatesForServices() { return currentSubscriptionEntitlementBlockingStatesForServices; } public boolean isEntitlementFutureCancelled() { return entitlementCancelEvent != null && entitlementCancelEvent.getEffectiveDate().isAfter(utcNow); } public boolean isEntitlementFutureChanged() { return getPendingSubscriptionEvents(utcNow, SubscriptionBaseTransitionType.CHANGE).iterator().hasNext(); } @Override public boolean isEntitlementActive() { return entitlementState == EntitlementState.ACTIVE; } @Override public boolean isEntitlementCancelled() { return entitlementState == EntitlementState.CANCELLED; } @Override public boolean isSubscriptionCancelled() { return subscription.getState() == EntitlementState.CANCELLED; } @Override public Collection<BlockingState> getBlockingStates() { return blockingStates; } @Override public Collection<BlockingState> getPendingEntitlementCancellationEvents() { return getPendingEntitlementEvents(DefaultEntitlementApi.ENT_STATE_CANCELLED); } @Override public BlockingState getEntitlementCancellationEvent() { return entitlementCancelEvent; } public Collection<BlockingState> getPendingEntitlementEvents(final String... types) { final List<String> typeList = ImmutableList.<String>copyOf(types); return Collections2.<BlockingState>filter(subscriptionEntitlementStates, new Predicate<BlockingState>() { @Override public boolean apply(final BlockingState input) { return !input.getEffectiveDate().isBefore(utcNow) && typeList.contains(input.getStateName()) && ( // ... for that subscription BlockingStateType.SUBSCRIPTION.equals(input.getType()) && input.getBlockedId().equals(subscription.getId()) || // ... for the associated base subscription BlockingStateType.SUBSCRIPTION.equals(input.getType()) && input.getBlockedId().equals(baseSubscription.getId()) || // ... for that bundle BlockingStateType.SUBSCRIPTION_BUNDLE.equals(input.getType()) && input.getBlockedId().equals(bundle.getId()) || // ... for that account BlockingStateType.ACCOUNT.equals(input.getType()) && input.getBlockedId().equals(account.getId())); } }); } public BlockingState getEntitlementCancellationEvent(final UUID subscriptionId) { return Iterables.<BlockingState>tryFind(subscriptionEntitlementStates, new Predicate<BlockingState>() { @Override public boolean apply(final BlockingState input) { return DefaultEntitlementApi.ENT_STATE_CANCELLED.equals(input.getStateName()) && input.getBlockedId().equals(subscriptionId); } }).orNull(); } public Iterable<SubscriptionBaseTransition> getPendingSubscriptionEvents(final DateTime effectiveDatetime, final SubscriptionBaseTransitionType... types) { final List<SubscriptionBaseTransitionType> typeList = ImmutableList .<SubscriptionBaseTransitionType>copyOf(types); return Iterables.<SubscriptionBaseTransition>filter(subscription.getAllTransitions(), new Predicate<SubscriptionBaseTransition>() { @Override public boolean apply(final SubscriptionBaseTransition input) { // Make sure we return the event for equality return !input.getEffectiveTransitionTime().isBefore(effectiveDatetime) && typeList.contains(input.getTransitionType()); } }); } @Override public Collection<BlockingState> computeAddonsBlockingStatesForNextSubscriptionBaseEvent( final DateTime effectiveDate) { return computeAddonsBlockingStatesForNextSubscriptionBaseEvent(effectiveDate, false); } // Compute future blocking states not on disk for add-ons associated to this (base) events stream @Override public Collection<BlockingState> computeAddonsBlockingStatesForFutureSubscriptionBaseEvents() { if (!ProductCategory.BASE.equals(subscription.getCategory())) { // Only base subscriptions have add-ons return ImmutableList.of(); } // We need to find the first "trigger" transition, from which we will create the add-ons cancellation events. // This can either be a future entitlement cancel... if (isEntitlementFutureCancelled()) { // Note that in theory we could always only look subscription base as we assume entitlement cancel means subscription base cancel // but we want to use the effective date of the entitlement cancel event to create the add-on cancel event final BlockingState futureEntitlementCancelEvent = getEntitlementCancellationEvent( subscription.getId()); return computeAddonsBlockingStatesForNextSubscriptionBaseEvent( futureEntitlementCancelEvent.getEffectiveDate(), false); } else if (isEntitlementFutureChanged()) { // ...or a subscription change (i.e. a change plan where the new plan has an impact on the existing add-on). // We need to go back to subscription base as entitlement doesn't know about these return computeAddonsBlockingStatesForNextSubscriptionBaseEvent(utcNow, true); } else { return ImmutableList.of(); } } private Collection<BlockingState> computeAddonsBlockingStatesForNextSubscriptionBaseEvent( final DateTime effectiveDate, final boolean useBillingEffectiveDate) { SubscriptionBaseTransition subscriptionBaseTransitionTrigger = null; if (!isEntitlementFutureCancelled()) { // Compute the transition trigger (either subscription cancel or change) final Iterable<SubscriptionBaseTransition> pendingSubscriptionBaseTransitions = getPendingSubscriptionEvents( effectiveDate, SubscriptionBaseTransitionType.CHANGE, SubscriptionBaseTransitionType.CANCEL); if (!pendingSubscriptionBaseTransitions.iterator().hasNext()) { return ImmutableList.<BlockingState>of(); } subscriptionBaseTransitionTrigger = pendingSubscriptionBaseTransitions.iterator().next(); } final Product baseTransitionTriggerNextProduct; final DateTime blockingStateEffectiveDate; if (subscriptionBaseTransitionTrigger == null) { baseTransitionTriggerNextProduct = null; blockingStateEffectiveDate = effectiveDate; } else { baseTransitionTriggerNextProduct = (EntitlementState.CANCELLED .equals(subscriptionBaseTransitionTrigger.getNextState()) ? null : subscriptionBaseTransitionTrigger.getNextPlan().getProduct()); blockingStateEffectiveDate = useBillingEffectiveDate ? subscriptionBaseTransitionTrigger.getEffectiveTransitionTime() : effectiveDate; } return computeAddonsBlockingStatesForSubscriptionBaseEvent(baseTransitionTriggerNextProduct, blockingStateEffectiveDate); } private Collection<BlockingState> computeAddonsBlockingStatesForSubscriptionBaseEvent( @Nullable final Product baseTransitionTriggerNextProduct, final DateTime blockingStateEffectiveDate) { if (baseSubscription == null || baseSubscription.getLastActivePlan() == null || !ProductCategory.BASE.equals(baseSubscription.getLastActivePlan().getProduct().getCategory())) { return ImmutableList.<BlockingState>of(); } // Compute included and available addons for the new product final Collection<String> includedAddonsForProduct; final Collection<String> availableAddonsForProduct; if (baseTransitionTriggerNextProduct == null) { includedAddonsForProduct = ImmutableList.<String>of(); availableAddonsForProduct = ImmutableList.<String>of(); } else { includedAddonsForProduct = Collections2.<Product, String>transform( ImmutableSet.<Product>copyOf(baseTransitionTriggerNextProduct.getIncluded()), new Function<Product, String>() { @Override public String apply(final Product product) { return product.getName(); } }); availableAddonsForProduct = Collections2.<Product, String>transform( ImmutableSet.<Product>copyOf(baseTransitionTriggerNextProduct.getAvailable()), new Function<Product, String>() { @Override public String apply(final Product product) { return product.getName(); } }); } // Retrieve all add-ons to block for that base subscription final Collection<SubscriptionBase> futureBlockedAddons = Collections2 .<SubscriptionBase>filter(allSubscriptionsForBundle, new Predicate<SubscriptionBase>() { @Override public boolean apply(final SubscriptionBase subscription) { final Plan lastActivePlan = subscription.getLastActivePlan(); final boolean result = ProductCategory.ADD_ON.equals(subscription.getCategory()) && // Check the subscription started, if not we don't want it, and that way we avoid doing NPE a few lines below. lastActivePlan != null && // Check the entitlement for that add-on hasn't been cancelled yet getEntitlementCancellationEvent(subscription.getId()) == null && ( // Base subscription cancelled baseTransitionTriggerNextProduct == null || ( // Change plan - check which add-ons to cancel includedAddonsForProduct.contains(lastActivePlan.getProduct().getName()) || !availableAddonsForProduct .contains(subscription.getLastActivePlan().getProduct().getName()))); return result; } }); // Create the blocking states return Collections2.<SubscriptionBase, BlockingState>transform(futureBlockedAddons, new Function<SubscriptionBase, BlockingState>() { @Override public BlockingState apply(final SubscriptionBase input) { return new DefaultBlockingState(input.getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, blockingStateEffectiveDate); } }); } private void setup() { computeEntitlementBlockingStates(); computeBlockingAggregator(); computeEntitlementEffectiveEndDate(); computeEntitlementCancelEvent(); computeStateForEntitlement(); } private void computeBlockingAggregator() { currentAccountEntitlementBlockingStatesForServices = filterCurrentBlockableStatePerService( accountEntitlementStates); currentBundleEntitlementBlockingStatesForServices = filterCurrentBlockableStatePerService( bundleEntitlementStates); currentSubscriptionEntitlementBlockingStatesForServices = filterCurrentBlockableStatePerService( subscriptionEntitlementStates); blockingAggregator = blockingChecker.getBlockedStatus(currentAccountEntitlementBlockingStatesForServices, currentBundleEntitlementBlockingStatesForServices, currentSubscriptionEntitlementBlockingStatesForServices, internalTenantContext); } private List<BlockingState> filterCurrentBlockableStatePerService( final Iterable<BlockingState> allBlockingStates) { final Map<String, BlockingState> currentBlockingStatePerService = new HashMap<String, BlockingState>(); for (final BlockingState blockingState : allBlockingStates) { if (blockingState.getEffectiveDate().isAfter(utcNow)) { continue; } if (currentBlockingStatePerService.get(blockingState.getService()) == null || currentBlockingStatePerService.get(blockingState.getService()).getEffectiveDate() .isBefore(blockingState.getEffectiveDate())) { currentBlockingStatePerService.put(blockingState.getService(), blockingState); } } return ImmutableList.<BlockingState>copyOf(currentBlockingStatePerService.values()); } private void computeEntitlementEffectiveEndDate() { LocalDate result = null; BlockingState lastEntry; lastEntry = (!subscriptionEntitlementStates.isEmpty()) ? subscriptionEntitlementStates.get(subscriptionEntitlementStates.size() - 1) : null; if (lastEntry != null && DefaultEntitlementApi.ENT_STATE_CANCELLED.equals(lastEntry.getStateName())) { result = new LocalDate(lastEntry.getEffectiveDate(), account.getTimeZone()); } lastEntry = (!bundleEntitlementStates.isEmpty()) ? bundleEntitlementStates.get(bundleEntitlementStates.size() - 1) : null; if (lastEntry != null && DefaultEntitlementApi.ENT_STATE_CANCELLED.equals(lastEntry.getStateName())) { final LocalDate localDate = new LocalDate(lastEntry.getEffectiveDate(), account.getTimeZone()); result = ((result == null) || (localDate.compareTo(result) < 0)) ? localDate : result; } lastEntry = (!accountEntitlementStates.isEmpty()) ? accountEntitlementStates.get(accountEntitlementStates.size() - 1) : null; if (lastEntry != null && DefaultEntitlementApi.ENT_STATE_CANCELLED.equals(lastEntry.getStateName())) { final LocalDate localDate = new LocalDate(lastEntry.getEffectiveDate(), account.getTimeZone()); result = ((result == null) || (localDate.compareTo(result) < 0)) ? localDate : result; } entitlementEffectiveEndDate = result; } private void computeEntitlementCancelEvent() { entitlementCancelEvent = Iterables .<BlockingState>tryFind(subscriptionEntitlementStates, new Predicate<BlockingState>() { @Override public boolean apply(final BlockingState input) { return DefaultEntitlementApi.ENT_STATE_CANCELLED.equals(input.getStateName()); } }).orNull(); } private void computeStateForEntitlement() { // Current state for the ENTITLEMENT_SERVICE_NAME is set to cancelled if (entitlementEffectiveEndDate != null && entitlementEffectiveEndDate.compareTo(new LocalDate(utcNow, account.getTimeZone())) <= 0) { entitlementState = EntitlementState.CANCELLED; } else { // Gather states across all services and check if one of them is set to 'blockEntitlement' entitlementState = (blockingAggregator != null && blockingAggregator.isBlockEntitlement() ? EntitlementState.BLOCKED : EntitlementState.ACTIVE); } } private void computeEntitlementBlockingStates() { subscriptionEntitlementStates = filterBlockingStatesForEntitlementService(BlockingStateType.SUBSCRIPTION, subscription.getId()); bundleEntitlementStates = filterBlockingStatesForEntitlementService(BlockingStateType.SUBSCRIPTION_BUNDLE, subscription.getBundleId()); accountEntitlementStates = filterBlockingStatesForEntitlementService(BlockingStateType.ACCOUNT, account.getId()); } private List<BlockingState> filterBlockingStatesForEntitlementService(final BlockingStateType blockingStateType, @Nullable final UUID blockableId) { return ImmutableList.<BlockingState>copyOf( Iterables.<BlockingState>filter(blockingStates, new Predicate<BlockingState>() { @Override public boolean apply(final BlockingState input) { return blockingStateType.equals(input.getType()) && EntitlementService.ENTITLEMENT_SERVICE_NAME.equals(input.getService()) && input.getBlockedId().equals(blockableId); } })); } }