com.comcast.locker.gateway.xgateway.service.ValidateLicenseController.java Source code

Java tutorial

Introduction

Here is the source code for com.comcast.locker.gateway.xgateway.service.ValidateLicenseController.java

Source

package com.comcast.locker.gateway.xgateway.service;

import com.comcast.common.Crn;
import com.comcast.common.Crn.Prefix;
import com.comcast.config.Configuration;
import com.comcast.locker.api.account.AccountId;
import com.comcast.locker.api.asset.AssetId;
import com.comcast.locker.api.asset.Right;
import com.comcast.locker.api.service.LockerService;
import com.comcast.locker.api.tx.Transaction;
import com.comcast.locker.gateway.services.GatewayService;
import com.comcast.locker.gateway.services.UnuService;
import com.comcast.locker.gateway.util.SettingUtils;
import com.comcast.locker.gateway.xgateway.XGatewaySettings;
import com.comcast.locker.gateway.xgateway.domain.ValidateLicenseRequest;
import com.comcast.locker.gateway.xgateway.domain.ValidateLicenseResponse;
import com.comcast.locker.gateway.xgateway.utils.XGatewayUtils;
import com.comcast.title.TitlePaid;
import com.comcast.title.Titles;
import com.comcast.title.encoding.Api.Rental;
import com.comcast.title.encoding.Api.Title;
import com.comcast.title.encoding.Api.TitlePlayability;
import com.comcast.util.Pair;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.List;
import java.util.Map;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Validate License Controller validates whether or not a given account
 * has a valid right to play a given asset.
 * <p>
 * If the account has a valid rental for it, the controller returns an entitlement.
 * If the account has the right to play the asset, but no rental, it will attempt
 * to rent the asset for the account.
 * Otherwise, it will return the appropriate reason the account cannot play the asset.
 */
public class ValidateLicenseController {

    private static final Logger LOG = LoggerFactory.getLogger(ValidateLicenseController.class);

    private static final String ENTITLEMENT_ID_PREFIX = "comcast:locker:xgateway:entitlement-id:";

    private static final String UNKNOWN_CLIENT_PART = "UNKNOWN";
    private static final ImmutableMap<String, String> CLIENT_OVERRIDES = new ImmutableMap.Builder<String, String>()
            .put("xcal:Xi3", "XRE:Xi3").put("xcal:X1", "XRE:X1").build();

    private static final long OWED_ASSET_EXPIRATION_TIME = 0;

    private final GatewayService gatewayService;
    private final UnuService unuService;
    private final LockerService lockerService;
    private final String cempTransactionHandler;

    private final long ensureBillingResumePointThreshold;
    private final long spdLockerTimeTolerance;
    private final long gracePeriodAllowance;

    private final String enforceHdcpValue;

    private final int minimumSecurityLevel;
    private final int minimumRuntimeSecurityLevel;

    private final boolean awaitEvidenceOfPlay;

    private final Map<String, String> extensions;

    public ValidateLicenseController(Configuration configuration, GatewayService gatewayService,
            LockerService lockerService, String cempTransactionHandler, UnuService unuService) {
        checkNotNull(gatewayService, "The 'gatewayService' cannot be null.");
        checkNotNull(unuService, "The 'unuService' cannot be null.");
        checkNotNull(lockerService, "The 'lockerService' cannot be null.");
        checkNotNull(cempTransactionHandler, "The 'cempTransactionHandler' cannot be null.");
        checkNotNull(configuration, "The 'configuration' cannot be null.");

        ensureBillingResumePointThreshold = configuration
                .getAsLong(XGatewaySettings.VALIDATE_LICENSE_ENSURE_BILLED_THESHOLD);
        spdLockerTimeTolerance = configuration
                .getAsLong(XGatewaySettings.VALIDATE_LICENSE_LICENSE_WINDOW_TOLERANCE);
        gracePeriodAllowance = configuration.getAsLong(XGatewaySettings.VALIDATE_LICENSE_GRACE_PERIOD_ALLOWANCE);
        enforceHdcpValue = configuration.get(XGatewaySettings.VALIDATE_LICENSE_ENFORCE_HDCP_VALUE);

        minimumSecurityLevel = configuration.getAsInt(XGatewaySettings.VALIDATE_LICENSE_MINIMUM_SECURITY_LEVEL);
        minimumRuntimeSecurityLevel = configuration
                .getAsInt(XGatewaySettings.VALIDATE_LICENSE_MINIMUM_RUNTIME_SECURITY_LEVEL);
        extensions = SettingUtils
                .settingValueAsMap(configuration.get(XGatewaySettings.VALIDATE_LICENSE_RIGHT_EXTENSIONS));
        awaitEvidenceOfPlay = configuration
                .getAsBoolean(XGatewaySettings.PURCHASE_AWAIT_EVIDENCE_OF_PLAY_BEFORE_BILLING);

        this.gatewayService = gatewayService;
        this.unuService = unuService;
        this.lockerService = lockerService;
        this.cempTransactionHandler = cempTransactionHandler;
    }

    /**
     * "Executes" the use case by:
     * Looking up assetId and accountIds, then checking for titlePlayability from UNU
     * Based on title playability decision, follow the following logic:
     * 1) If bad decision - fail
     * 2) If rental - success
     * 3) If not rental
     *    3a) check for rights - if owned, success (and add to SPD)
     *    3b) if free/gated - success (and add to SPD)
     *    3c) if transaction - check locker
     *        3c)a) if locker, success (and add to spd)
     *        3c)b) if not in locker - error 'Not Purchased'
     */
    public ValidateLicenseResponse validateLicense(ValidateLicenseRequest validateLicenseRequest) {
        checkArgument(validateLicenseRequest != null, "The 'purchaseRequest' cannot be null.");

        long now = System.currentTimeMillis();

        Pair<Crn, AccountId> accountPair = getAccountId(validateLicenseRequest);
        AccountId accountId = accountPair.getSecond();
        AssetId assetId = getAssetId(validateLicenseRequest);
        String client = getClient(validateLicenseRequest);

        String accountNumber = gatewayService.resolveAccountNumberFromLockerAccountId(accountId);
        TitlePaid paid = gatewayService.resolveTitlePaidFromLockerAssetId(assetId);

        //step 1, check on title playability
        TitlePlayability playability = unuService.validatePlayEligibility(paid, accountNumber, client);

        //validate decisions and throw if bad decision
        validateAvailability(playability);

        Title title = playability.getTitle();

        //step 2, check if the title is in SPD (if it is it'll have a rental object attached)
        if (title.hasRental()) {
            //if we have a rental, use it's expiration date and it's start date for the
            //entitlement
            Rental rental = title.getRental();
            ensureTitleWasBilled(client, rental, title, accountId, assetId, now,
                    new Crn(Prefix.TITLE_PAID, paid.getRawPaid()), accountPair.getFirst());
            long expirationDate = rental.getExpireDateTime();

            return generateResponse(accountNumber, title, new Date(rental.getRentDateTime()),
                    new Date(expirationDate), title.hasRight());
        }

        //if user is entitled to the asset because it's free, gated by something the user
        //is entitled to, or if the user owns it
        if (Titles.titleIsFree(title) || Titles.titleIsGatedAndSubscriberIsEntitled(title) || title.hasRight()) {

            return generateResponse(accountNumber, title, new Date(now), null, title.hasRight());
        }
        List<Right> lockerRights = lockerService.getAssetRightsByAccountAndAsset(accountId, assetId);
        for (Right right : lockerRights) {
            if (right.isInWindow(now) && right.isPlayable()) {
                //we bought or rented this and the locker knows about it but not saved programs
                //lets let them watch it and try to save it as well to make this easier the
                //next time around

                return generateResponse(accountNumber, title, right.getStartTime(), right.getEndTime(),
                        right.isPermanent());
            }
        }

        if (Titles.titleIsElectronicSellThrough(title)) {
            throw new AssetNotPurchasedException("Asset not Purchased");
        } else {
            throw new AssetNotPurchasedException("Asset not Rented");
        }
    }

    @VisibleForTesting
    String getClient(ValidateLicenseRequest request) {
        String provider = request.getSubjectInfo().getProvider();
        String model = request.getSubjectInfo().getDeviceModel();
        String clientKey;
        if (model == null && provider == null) {
            //if both empty, keep it short and sweet
            clientKey = UNKNOWN_CLIENT_PART;
        } else {
            clientKey = StringUtils.defaultIfEmpty(provider, UNKNOWN_CLIENT_PART) + ':'
                    + StringUtils.defaultIfEmpty(model, UNKNOWN_CLIENT_PART);
        }
        String client = CLIENT_OVERRIDES.get(clientKey);
        if (client == null) {
            LOG.warn("Received unexpected client fields " + clientKey);
            return clientKey;
        }
        return client;

    }

    private ValidateLicenseResponse generateResponse(String accountNumber, Title title, Date availableDate,
            Date expirationDateFromStorage, boolean owned) {

        Date expirationDate = expirationDateFromStorage;
        if (title.getAllowedRentalDurationInSeconds() == 0) {
            expirationDate = new Date(availableDate.getTime() + XGatewayUtils.parseDuration(title.getRunTime()));
        } else if (owned) {
            expirationDate = new Date(OWED_ASSET_EXPIRATION_TIME);
        }
        if (expirationDate == null) {
            expirationDate = XGatewayUtils.calculateEndTime(availableDate.getTime(),
                    title.getAllowedRentalDurationInSeconds());
        }

        ValidateLicenseResponse res = new ValidateLicenseResponse();
        res.setEntitlementId(generateEntitlementId(accountNumber, title.getPaid(), availableDate.getTime()));
        res.setExpirationDate(expirationDate);
        res.setEnforceHdcp(enforceHdcpValue);

        res.setMinimumSecurityLevel(minimumSecurityLevel);
        res.setMinimumRuntimeSecurityLevel(minimumRuntimeSecurityLevel);
        res.setAvailableDate(availableDate);
        res.setRightsExtensions(extensions);

        return res;
    }

    /**
     * {@code accountNumber + assetId + rentalStartTime}.
     * Only included in successful responses.
     * <br/>
     * e.g.
     * <code>
     * comcast:locker:xgateway:entitlement-id:9645225921090-hbo.comTITL00000000000000001-1213087203740234
     * </code>
     */
    private String generateEntitlementId(String accountNumber, String paid, long rentalStartTime) {
        StringBuilder sb = new StringBuilder(ENTITLEMENT_ID_PREFIX);
        sb.append(accountNumber).append('-').append(paid).append('-').append(rentalStartTime);
        return sb.toString();
    }

    /**
     * Retrieves the accountId from the ValidateLicenseRequest, first attempting to use the
     * CET Billing ID, but then trying with the AuthGuid if the Billing Id is blank
     */
    private Pair<Crn, AccountId> getAccountId(ValidateLicenseRequest request) {

        String cetBillingId = request.getSubjectInfo().getCetBillingId();
        if (!StringUtils.isEmpty(cetBillingId)) {
            return new Pair<>(new Crn(Prefix.CET_ACCOUNT_NUMBER, cetBillingId),
                    gatewayService.resolveLockerAccountIdFromAccountNumber(cetBillingId));
        } else if (!StringUtils.isEmpty(request.getSubjectInfo().getCetAuthGuid())) {
            String authGuid = request.getSubjectInfo().getCetAuthGuid();
            return new Pair<>(new Crn(Prefix.CET_AUTH_GUID, authGuid),
                    gatewayService.resolveLockerAccountIdFromAuthGuid(authGuid));
        } else if (!StringUtils.isEmpty(request.getSubjectInfo().getPartnerBillingId())) {
            String partnerBillingId = request.getSubjectInfo().getPartnerBillingId();
            return new Pair<>(new Crn(Prefix.CET_ACCOUNT_NUMBER, partnerBillingId),
                    gatewayService.resolveLockerAccountIdFromAccountNumber(partnerBillingId));
        } else {
            String partnerAuthGuid = request.getSubjectInfo().getPartnerAuthGuid();
            return new Pair<>(new Crn(Prefix.CET_AUTH_GUID, partnerAuthGuid),
                    gatewayService.resolveLockerAccountIdFromAuthGuid(partnerAuthGuid));
        }

    }

    /**
     * Ensures the title was billed using the following logic:
     * Checks XGateway Configuration for resumepoint threshold for billing.
     * If the title's resume point doesn't have a resume point greater than the
     * threshold, we check the locker for any rights in window, if no rights,
     * we re-add the transaction to ensure billing
     * <p>
     * If the threshold is set to -1, that means we always check the locker
     */
    private void ensureTitleWasBilled(String client, Rental rental, Title title, AccountId accountId,
            AssetId assetId, long now, Crn assetCrn, Crn accountCrn) {
        if (title.hasRight() || Titles.titleIsElectronicSellThrough(title) || Titles.titleIsFree(title)
                || Titles.titleIsGatedAndSubscriberIsEntitled(title)) {
            //if a title is in SPD, then we don't rebill (or first bill) for ESTs, Free assets or gated assets
            return;
        }
        if (ensureBillingResumePointThreshold == -1
                || rental.getResumePointInMillis() <= ensureBillingResumePointThreshold) {
            //if the locker expired and we're in the grace period, allow a tolerance of the gracePeriodAllowance,
            //otherwise, if we're in the rental window, only allow for differences in the times of
            //the different servers

            //if rental is expired, but we made it here, that means we're in grace period
            //grace periods have a larger allowance for locker/spd differences
            long tolerance = rental.getExpireDateTime() < now ? gracePeriodAllowance : spdLockerTimeTolerance;
            List<Right> rights = lockerService.getAssetRightsByAccountAndAsset(accountId, assetId);
            for (Right right : rights) {
                if (right.isInWindow(now, tolerance)) {
                    return;
                }
            }
            //if we're here, we don't have a right, we're not in the locker
            //and we haven't watched it long enough to avoid all this, create a new transaction
            createPurchaseTransactionInLocker(client, title, accountId, assetId, assetCrn, accountCrn);
        }
    }

    private AssetId getAssetId(ValidateLicenseRequest request) {
        String providerId = request.getContentMetadataInfo().getProviderId();
        String assetId = request.getContentMetadataInfo().getTitleAssetId();
        return gatewayService.resolveLockerAssetIdFromTitlePaid(new TitlePaid(providerId, assetId));
    }

    private Pair<Transaction, Date> createPurchaseTransactionInLocker(String client, Title title,
            AccountId accountId, AssetId assetId, Crn assetCrn, Crn accountCrn) {

        Pair<Transaction, Date> trans = XGatewayUtils.createPurchaseTransactionForLocker(client,
                cempTransactionHandler, null, title, accountId, assetId, assetCrn, accountCrn, awaitEvidenceOfPlay);

        Date expiration = trans.getSecond();
        Transaction lockerTrans = lockerService.createTransaction(trans.getFirst());

        return new Pair<>(lockerTrans, expiration);
    }

    private void validateAvailability(TitlePlayability titlePlayability) {
        switch (titlePlayability.getDecision()) {
        case PLAYABLE_TITLE:
        case PLAYABLE_PREVIEW:
        case PLAYABLE_NDVR_TITLE:
            //Anything playable is good
            break;
        case NOT_AVAILABLE_ON_VOD_SYSTEM:
            throw new AssetIsNotAvailableException("Asset not available on CCDN.");
        case NOT_IN_WINDOW:
            throw new AssetIsNotAvailableException("License window expired on %1$tm/%1$td/%1$tY.",
                    new Date(titlePlayability.getTitle().getLicenseWindow().getEnd()));
        case NOT_ENTITLED:
            throw new NotEntitledException("Not entitled to %s.",
                    titlePlayability.getTitle().getService().getServiceCode());
        case NOT_ALLOWED_VIA_DL:
            throw new AssetIsNotAvailableException("Asset is restricted by distribution list.");
        case NOT_ALLOWED_VIA_MENU_ITEM:
            throw new AssetIsNotAvailableException("Asset is not available on the VOD menu.");
        case NOT_ALLOWED_FOR_CREDIT_HOLD:
            throw new AccountNotInGoodStandingException("Account on credit hold.");
        case NOT_ALLOWED_FOR_NO_CHARGES_ON_ACCOUNT:
            throw new AccountNotInGoodStandingException("No charges allowed on account");
        case NOT_ALLOWED_FOR_INVALID_HIBIT:
            throw new AccountNotInGoodStandingException("Account has invalid hibit.");

        case NOT_ALLOWED_REQUIRES_HDMI:
            throw new AssetIsNotAvailableException("Asset requires HDMI.");

        }

    }

}