// Copyright 2017 Google Inc. All Rights Reserved. // // Licensed 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.google.api.ads.adwords.axis.utils.v201702.shopping; import com.google.api.ads.adwords.axis.utils.ProductPartitionTreeInterface; import com.google.api.ads.adwords.axis.utils.v201702.SelectorBuilder; import com.google.api.ads.adwords.axis.utils.v201702.shopping.ProductPartitionNodeDiffer.NodeDifference; import com.google.api.ads.adwords.axis.v201702.cm.AdGroup; import com.google.api.ads.adwords.axis.v201702.cm.AdGroupCriterion; import com.google.api.ads.adwords.axis.v201702.cm.AdGroupCriterionOperation; import com.google.api.ads.adwords.axis.v201702.cm.AdGroupCriterionPage; import com.google.api.ads.adwords.axis.v201702.cm.AdGroupCriterionServiceInterface; import com.google.api.ads.adwords.axis.v201702.cm.AdGroupPage; import com.google.api.ads.adwords.axis.v201702.cm.AdGroupServiceInterface; import com.google.api.ads.adwords.axis.v201702.cm.ApiException; import com.google.api.ads.adwords.axis.v201702.cm.BidSource; import com.google.api.ads.adwords.axis.v201702.cm.BiddableAdGroupCriterion; import com.google.api.ads.adwords.axis.v201702.cm.BiddingStrategyConfiguration; import com.google.api.ads.adwords.axis.v201702.cm.Bids; import com.google.api.ads.adwords.axis.v201702.cm.CpcBid; import com.google.api.ads.adwords.axis.v201702.cm.Money; import com.google.api.ads.adwords.axis.v201702.cm.Operator; import com.google.api.ads.adwords.axis.v201702.cm.ProductDimension; import com.google.api.ads.adwords.axis.v201702.cm.ProductPartition; import com.google.api.ads.adwords.axis.v201702.cm.ProductPartitionType; import com.google.api.ads.adwords.axis.v201702.cm.Selector; import com.google.api.ads.adwords.axis.v201702.cm.UserStatus; import com.google.api.ads.adwords.lib.client.AdWordsSession; import com.google.api.ads.adwords.lib.factory.AdWordsServicesInterface; import com.google.api.ads.adwords.lib.selectorfields.v201702.cm.AdGroupCriterionField; import com.google.api.ads.adwords.lib.selectorfields.v201702.cm.AdGroupField; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.AbstractSequentialIterator; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import java.rmi.RemoteException; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; /** * Implementation of {@link ProductPartitionTreeInterface} for Axis v201702. */ class ProductPartitionTreeImpl implements ProductPartitionTreeInterface<ProductPartitionNode, AdGroupCriterionOperation> { /** A generator for temporary (negative) product partition IDs. */ private final Iterator<Long> idGenerator; /** The ID of the {@link AdGroup} of this tree. */ private final long adGroupId; private final BiddingStrategyConfiguration biddingStrategyConfig; /** The root node of this tree. */ private final ProductPartitionNode root; /** * The <em>original</em> root node of this tree. This will be null if this tree's ad group * originally contained no nodes, e.g., the ad group was created via the API. Otherwise, it will * be a deep copy of the ad group's original root node. * * <p>This root will be used to detect changes made to the tree under {@code root}. See * {@link #createMutateOperationPairs()}. */ private final ProductPartitionNode originalRoot; private final Comparator<ProductDimension> dimensionComparator; /** * The page size to use when retrieving ad group criteria. */ private static final int PAGE_SIZE = 100; /** * Required fields for any {@link Selector} used to fetch {@link AdGroupCriterion} objects used by * an instance of this class. */ static final ImmutableList<AdGroupCriterionField> REQUIRED_SELECTOR_FIELD_ENUMS = ImmutableList.of( AdGroupCriterionField.AdGroupId, AdGroupCriterionField.Id, AdGroupCriterionField.ParentCriterionId, AdGroupCriterionField.PartitionType, AdGroupCriterionField.CriteriaType, AdGroupCriterionField.CaseValue, AdGroupCriterionField.CpcBid, AdGroupCriterionField.CpcBidSource, AdGroupCriterionField.Status); /** * Constructor that initializes the temp ID generator based on the ID of the root node. * * @param adGroupId the ID of the ad group * @param biddingStrategyConfig the bidding strategy configuration of the ad group * @param rootNode the root node of the tree */ ProductPartitionTreeImpl(long adGroupId, BiddingStrategyConfiguration biddingStrategyConfig, ProductPartitionNode rootNode) { this.adGroupId = adGroupId; this.biddingStrategyConfig = Preconditions.checkNotNull(biddingStrategyConfig, "Null bidding strategy configuration"); this.root = Preconditions.checkNotNull(rootNode, "Null root node"); long startingTempId; this.dimensionComparator = new ProductDimensionComparator(); if (this.root.getProductPartitionId() < 0L) { // The root has a temporary ID, so all changes made to this tree should result in ADD // operations. originalRoot = null; startingTempId = -1L; } else { // Set originalRoot to a deep copy of the root node. originalRoot = new ProductPartitionNode(null, root.getDimension(), root.getProductPartitionId(), this.dimensionComparator); long minimumId = cloneChildrenToNewParent(originalRoot, root.getChildren(), originalRoot.getProductPartitionId()); // The starting temp ID should be -1 if all nodes are non-temporary (have positive IDs), // else start at one less than the lowest ID found in the tree. startingTempId = minimumId >= 0L ? -1L : minimumId - 1L; } this.idGenerator = new AbstractSequentialIterator<Long>(startingTempId) { @Override protected Long computeNext(Long previous) { return Long.MIN_VALUE == previous.longValue() ? null : previous - 1; } }; } /** * Deeply clones each child in {@code children} and attaches it to {@code newParent}. * * @param newParent the new parent to which the cloned children will be added * @param children the children to clone * @param minimumId the minimum ID to compare to - may be null * * @return the minimum product partition ID found within the subtrees under {@code children} */ private static long cloneChildrenToNewParent(ProductPartitionNode newParent, Iterable<ProductPartitionNode> children, final Long minimumId) { long updatedMinimumId = minimumId == null ? Long.MAX_VALUE : minimumId; for (ProductPartitionNode childNode : children) { newParent.asSubdivision(); // Clone the child and add it to newParent's collection of children. ProductPartitionNode newChild = newParent.addChild(childNode.getDimension()); newChild = ProductPartitionNode.copyProperties(childNode, newChild); updatedMinimumId = Math.min(updatedMinimumId, newChild.getProductPartitionId() == null ? Long.MAX_VALUE : newChild.getProductPartitionId()); // Recursively clone the child's children. updatedMinimumId = cloneChildrenToNewParent(newChild, childNode.getChildren(), updatedMinimumId); } return updatedMinimumId; } /** * Returns a new instance of this class by retrieving the product partitions of the * specified ad group. All parameters are required. */ static ProductPartitionTreeImpl createAdGroupTree(AdWordsServicesInterface services, AdWordsSession session, Long adGroupId) throws ApiException, RemoteException { // Get the AdGroupCriterionService. AdGroupCriterionServiceInterface criterionService = services.get(session, AdGroupCriterionServiceInterface.class); SelectorBuilder selectorBuilder = new SelectorBuilder() .fields(REQUIRED_SELECTOR_FIELD_ENUMS.toArray( new AdGroupCriterionField[REQUIRED_SELECTOR_FIELD_ENUMS.size()])) .equals(AdGroupCriterionField.AdGroupId, adGroupId.toString()) .equals(AdGroupCriterionField.CriteriaType, "PRODUCT_PARTITION") .in( AdGroupCriterionField.Status, UserStatus.ENABLED.getValue(), UserStatus.PAUSED.getValue()) .limit(PAGE_SIZE); AdGroupCriterionPage adGroupCriterionPage; // A multimap from each product partition ID to its direct children. ListMultimap<Long, AdGroupCriterion> parentIdMap = LinkedListMultimap.create(); int offset = 0; do { // Get the next page of results. adGroupCriterionPage = criterionService.get(selectorBuilder.build()); if (adGroupCriterionPage != null && adGroupCriterionPage.getEntries() != null) { for (AdGroupCriterion adGroupCriterion : adGroupCriterionPage.getEntries()) { ProductPartition partition = (ProductPartition) adGroupCriterion.getCriterion(); parentIdMap.put(partition.getParentCriterionId(), adGroupCriterion); } offset += adGroupCriterionPage.getEntries().length; selectorBuilder.increaseOffsetBy(PAGE_SIZE); } } while (offset < adGroupCriterionPage.getTotalNumEntries()); // Construct the ProductPartitionTree from the parentIdMap. if (!parentIdMap.containsKey(null)) { Preconditions.checkState(parentIdMap.isEmpty(), "No root criterion found in the tree but the tree is not empty"); return createEmptyAdGroupTree(adGroupId, getAdGroupBiddingStrategyConfiguration(services, session, adGroupId)); } return createNonEmptyAdGroupTree(adGroupId, parentIdMap); } /** * Returns a new instance of this class based on the collection of ad group criteria provided. * <p>NOTE: If retrieving existing criteria for use with this method, you must include all of the * fields in {@link #REQUIRED_SELECTOR_FIELD_ENUMS} in your {@link Selector}. * * @param adGroupId the ID of the ad group * @param biddingStrategyConfig the {@link BiddingStrategyConfiguration} for the ad group * @param adGroupCriteria the non-null (but possibly empty) list of ad group criteria * * @throws NullPointerException if any argument is null, any element in {@code adGroupCriteria} is * null, or any required field from {@link #REQUIRED_SELECTOR_FIELD_ENUMS} is missing from * an element in {@code adGroupCriteria} * @throws IllegalArgumentException if {@code adGroupCriteria} does not include the root criterion * of the product partition tree */ static ProductPartitionTreeImpl createAdGroupTree(Long adGroupId, BiddingStrategyConfiguration biddingStrategyConfig, List<AdGroupCriterion> adGroupCriteria) { Preconditions.checkNotNull(adGroupId, "Null ad group ID"); Preconditions.checkNotNull(biddingStrategyConfig, "Null bidding strategy configuration"); Preconditions.checkNotNull(adGroupCriteria, "Null criteria list"); if (adGroupCriteria.isEmpty()) { return createEmptyAdGroupTree(adGroupId, biddingStrategyConfig); } ListMultimap<Long, AdGroupCriterion> parentIdMap = LinkedListMultimap.create(); for (AdGroupCriterion adGroupCriterion : adGroupCriteria) { Preconditions.checkNotNull(adGroupCriterion.getCriterion(), "AdGroupCriterion has a null criterion"); if (adGroupCriterion instanceof BiddableAdGroupCriterion) { BiddableAdGroupCriterion biddableCriterion = (BiddableAdGroupCriterion) adGroupCriterion; Preconditions.checkNotNull(biddableCriterion.getUserStatus(), "User status is null for criterion ID %s", biddableCriterion.getCriterion().getId()); if (UserStatus.REMOVED.equals(biddableCriterion.getUserStatus())) { // Skip REMOVED criteria. continue; } } if (adGroupCriterion.getCriterion() instanceof ProductPartition) { ProductPartition partition = (ProductPartition) adGroupCriterion.getCriterion(); parentIdMap.put(partition.getParentCriterionId(), adGroupCriterion); } } return createNonEmptyAdGroupTree(adGroupId, parentIdMap); } /** * Returns a new tree based on a non-empty collection of ad group criteria. All parameters * required. * * @param adGroupId the ID of the ad group * @param parentIdMap the multimap from parent product partition ID to child criteria * @return a new ProductPartitionTree */ private static ProductPartitionTreeImpl createNonEmptyAdGroupTree(Long adGroupId, ListMultimap<Long, AdGroupCriterion> parentIdMap) { Preconditions.checkNotNull(adGroupId, "Null ad group ID"); Preconditions.checkArgument(!parentIdMap.isEmpty(), "parentIdMap passed for ad group ID %s is empty", adGroupId); Preconditions.checkArgument(parentIdMap.containsKey(null), "No root criterion found in the list of ad group criteria for ad group ID %s", adGroupId); AdGroupCriterion rootCriterion = Iterables.getOnlyElement(parentIdMap.get(null)); Preconditions.checkState(rootCriterion instanceof BiddableAdGroupCriterion, "Root criterion for ad group ID %s is not a BiddableAdGroupCriterion", adGroupId); BiddableAdGroupCriterion biddableRootCriterion = (BiddableAdGroupCriterion) rootCriterion; BiddingStrategyConfiguration biddingStrategyConfig = biddableRootCriterion.getBiddingStrategyConfiguration(); Preconditions.checkState(biddingStrategyConfig != null, "Null bidding strategy config on the root node of ad group ID %s", adGroupId); ProductPartitionNode rootNode = new ProductPartitionNode(null, (ProductDimension) null, rootCriterion.getCriterion().getId(), new ProductDimensionComparator()); // Set the root's bid if a bid exists on the BiddableAdGroupCriterion. Money rootNodeBid = getBid(biddableRootCriterion); if (rootNodeBid != null) { rootNode = rootNode.asBiddableUnit().setBid(rootNodeBid.getMicroAmount()); } addChildNodes(rootNode, parentIdMap); return new ProductPartitionTreeImpl(adGroupId, biddingStrategyConfig, rootNode); } /** * Returns a new empty tree. * * @param adGroupId the ID of the ad group * @param biddingStrategyConfig the bidding strategy configuration of the ad group */ private static ProductPartitionTreeImpl createEmptyAdGroupTree(Long adGroupId, BiddingStrategyConfiguration biddingStrategyConfig) { Preconditions.checkNotNull(adGroupId, "Null ad group ID"); Preconditions.checkNotNull(biddingStrategyConfig, "Null bidding strategy configuration"); ProductPartitionNode rootNode = new ProductPartitionNode(null, null, -1L, new ProductDimensionComparator()); return new ProductPartitionTreeImpl(adGroupId, biddingStrategyConfig, rootNode); } @Override public Long getAdGroupId() { return adGroupId; } @Override public ProductPartitionNode getRoot() { return root; } @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) .append("adGroupId", adGroupId) .append("biddingStrategyConfig", biddingStrategyConfig) .append("originalRoot is empty", originalRoot == null) // Add a newline before the root so that the tree levels all line up. .append("root", String.format("%n%s", root.toDetailedString())) .toString(); } @Override public List<AdGroupCriterionOperation> getMutateOperations() { return Lists.transform(createMutateOperationPairs(), new Function<OperationPair, AdGroupCriterionOperation>() { @Override public AdGroupCriterionOperation apply(OperationPair input) { return input.operation; } }); } /** * Creates and returns the pairs of node/operation that will apply the changes made to this tree. */ private List<OperationPair> createMutateOperationPairs() { List<OperationPair> ops = Lists.newArrayList(); if (originalRoot == null) { // The original tree was empty, so treat all nodes in this tree as ADDs. return createAddOperations(root); } // Add the mutate operations required to apply changes to the root node. Set<ProductDimension> dimensionsToProcess = addMutateOperations(originalRoot, root, ops); // Add additional mutate operations for the remaining child dimensions to process. addMutateOperationsByParent(originalRoot, root, dimensionsToProcess, ops); return ops; } /** * Adds to the operations list all operations required to mutate the children of * {@code originalParentNode} to {@code newParentNode}. * * @param originalParentNode required - must not be null * @param newParentNode required - must not be null * @param childDimensionsToProcess the child dimensions to process * @param ops the operations list to add to */ private void addMutateOperationsByParent(ProductPartitionNode originalParentNode, ProductPartitionNode newParentNode, Set<ProductDimension> childDimensionsToProcess, List<OperationPair> ops) { for (ProductDimension dimensionToProcess : childDimensionsToProcess) { ProductPartitionNode originalChild = originalParentNode.hasChild(dimensionToProcess) ? originalParentNode.getChild(dimensionToProcess) : null; ProductPartitionNode newChild = newParentNode.hasChild(dimensionToProcess) ? newParentNode.getChild(dimensionToProcess) : null; Set<ProductDimension> grandchildDimensionsToProcess = addMutateOperations(originalChild, newChild, ops); if (!grandchildDimensionsToProcess.isEmpty()) { // Logic check - the only condition where further processing of children is required // is when the parent exists in both trees. If the parent is null in one tree but // not the other, then the node for dimensionToProcess was either: // 1) removed from the original OR // 2) added to the new tree // In both cases, the call to addMutateOperations above will have already added all of the // necessary operations to handle the node and all of its children. Preconditions.checkState(originalChild != null, "Original child should not be null if there are children to process"); Preconditions.checkState(newChild != null, "New child should not be null if there are children to process"); addMutateOperationsByParent(originalChild, newChild, grandchildDimensionsToProcess, ops); } } } /** * Adds to the operations list all operations required to mutate {@code originalNode} to the state * of {@code newNode}. * * <p>The returned set of child product dimensions will only <em>potentially</em> be non-empty if * both {@code originalNode != null} and {@code newNode != null}. * * @param originalNode may be null * @param newNode may be null * @param ops the operations list to add to * * @return the set of child product dimensions that require further processing */ private Set<ProductDimension> addMutateOperations(@Nullable ProductPartitionNode originalNode, @Nullable ProductPartitionNode newNode, List<OperationPair> ops) { Set<ProductDimension> childDimensionsToProcess = Sets.newTreeSet(dimensionComparator); NodeDifference nodeDifference = ProductPartitionNodeDiffer.diff(originalNode, newNode, dimensionComparator); boolean isProcessChildren; switch (nodeDifference) { case NEW_NODE: ops.addAll(createAddOperations(newNode)); // No need to further process children. The ADD operations above will include operations // for all children of newNode. isProcessChildren = false; break; case REMOVED_NODE: ops.add(createRemoveOperation(originalNode)); // No need to further process children. The REMOVE operation above will perform a // cascading delete of all children of newNode. isProcessChildren = false; break; case PARTITION_TYPE_CHANGE: case EXCLUDED_UNIT_CHANGE: ops.add(createRemoveOperation(originalNode)); ops.addAll(createAddOperations(newNode)); // No need to further process children. The ADD operations above will include operations // for all children of newNode. isProcessChildren = false; break; case BID_CHANGE: // Ensure that the new node has the proper ID (this may have been lost if the node // was removed and then re-added). newNode = newNode.setProductPartitionId(originalNode.getProductPartitionId()); ops.add(createSetBidOperation(newNode)); // Process the children of newNode. The SET operation above will only handle changes // made to newNode, not its children. isProcessChildren = true; break; case NONE: // Ensure that the new node has the proper ID (this may have been lost if the node // was removed and then re-added). newNode = newNode.setProductPartitionId(originalNode.getProductPartitionId()); // This node does not have changes, but its children may. isProcessChildren = true; break; default: throw new IllegalStateException("Unrecognized difference: " + nodeDifference); } if (isProcessChildren) { for (ProductPartitionNode childNode : Iterables.concat(originalNode.getChildren(), newNode.getChildren())) { childDimensionsToProcess.add(childNode.getDimension()); } } return childDimensionsToProcess; } /** * Returns a SET operation for the specified node. */ private OperationPair createSetBidOperation(ProductPartitionNode node) { Preconditions.checkNotNull(node.getProductPartitionId(), "Node for SET operation has no partition ID: %s", node); Preconditions.checkArgument(node.getProductPartitionId().longValue() >= 0L, "Node for SET operation has a negative partition ID: %s", node); AdGroupCriterionOperation setOp = new AdGroupCriterionOperation(); setOp.setOperator(Operator.SET); setOp.setOperand(ProductPartitionNodeAdapter.createCriterionForSetBid(node, adGroupId, getBiddingStrategyConfiguration())); return new OperationPair(node, setOp); } /** * Creates ADD operations for the node and ALL of its children and adds them to the provided * operations list. */ private List<OperationPair> createAddOperations(ProductPartitionNode node) { AdGroupCriterionOperation addOp = new AdGroupCriterionOperation(); addOp.setOperator(Operator.ADD); // Set the node's ID to a new temporary ID. node.setProductPartitionId(idGenerator.next()); addOp.setOperand(ProductPartitionNodeAdapter.createCriterionForAdd(node, adGroupId, getBiddingStrategyConfiguration())); List<OperationPair> operationsList = Lists.newArrayList(); operationsList.add(new OperationPair(node, addOp)); // Recursively add all of this node's children to the operations list. for (ProductPartitionNode child : node.getChildren()) { operationsList.addAll(createAddOperations(child)); } return operationsList; } /** * Returns a REMOVE operation for the specified node. */ private OperationPair createRemoveOperation(ProductPartitionNode node) { Preconditions.checkNotNull(node.getProductPartitionId(), "Node for REMOVE operation has no partition ID: %s", node); Preconditions.checkArgument(node.getProductPartitionId().longValue() >= 0L, "Node for REMOVE operation has a negative partition ID: %s", node); AdGroupCriterionOperation removeOp = new AdGroupCriterionOperation(); removeOp.setOperator(Operator.REMOVE); removeOp.setOperand(ProductPartitionNodeAdapter.createCriterionForRemove(node, adGroupId)); return new OperationPair(node, removeOp); } /** * Returns a copy of this tree's bidding strategy configuration. */ private BiddingStrategyConfiguration getBiddingStrategyConfiguration() { // Create a copy of the config. BiddingStrategyConfiguration copy = new BiddingStrategyConfiguration(); return copy; } /** * An OperartionPair associates a ProductPartitionNode with an AdGroupCriterionOperation that * mutates the node. */ private static class OperationPair { final ProductPartitionNode node; final AdGroupCriterionOperation operation; OperationPair(ProductPartitionNode node, AdGroupCriterionOperation operation) { this.node = node; this.operation = operation; } @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("node", node) .append("operation", operation).toString(); } } /** * Retrieves the {@link BiddingStrategyConfiguration} of an ad group. * * @param services the AdWordsServices * @param session the session to use for the request * @param adGroupId the ad group ID * @return the non-null BiddingStrategyConfiguration of the ad group */ private static BiddingStrategyConfiguration getAdGroupBiddingStrategyConfiguration( AdWordsServicesInterface services, AdWordsSession session, Long adGroupId) throws ApiException, RemoteException { AdGroupServiceInterface adGroupService = services.get(session, AdGroupServiceInterface.class); Selector selector = new SelectorBuilder() .fields( AdGroupField.Id, AdGroupField.BiddingStrategyType, AdGroupField.BiddingStrategyId, AdGroupField.BiddingStrategyName) .equalsId(adGroupId).build(); AdGroupPage adGroupPage = adGroupService.get(selector); if (adGroupPage.getEntries() == null || adGroupPage.getEntries().length == 0) { throw new IllegalArgumentException("No ad group found with ID " + adGroupId); } AdGroup adGroup = adGroupPage.getEntries(0); Preconditions.checkState(adGroup.getBiddingStrategyConfiguration() != null, "Unexpected state - ad group ID %s has a null BiddingStrategyConfiguration", adGroupId); return adGroup.getBiddingStrategyConfiguration(); } /** * Using the criteria in {@code parentIdMap}, recursively adds all children under the partition ID * of {@code parentNode} to {@code parentNode}. * * @param parentNode required * @param parentIdMap the multimap from parent partition ID to list of child criteria */ private static void addChildNodes(ProductPartitionNode parentNode, ListMultimap<Long, AdGroupCriterion> parentIdMap) { if (parentIdMap.containsKey(parentNode.getProductPartitionId())) { parentNode = parentNode.asSubdivision(); } for (AdGroupCriterion adGroupCriterion : parentIdMap.get(parentNode.getProductPartitionId())) { ProductPartition partition = (ProductPartition) adGroupCriterion.getCriterion(); ProductPartitionNode childNode = parentNode.addChild(partition.getCaseValue()); childNode = childNode.setProductPartitionId(partition.getId()); if (ProductPartitionType.SUBDIVISION.equals(partition.getPartitionType())) { childNode = childNode.asSubdivision(); } else { if (adGroupCriterion instanceof BiddableAdGroupCriterion) { childNode = childNode.asBiddableUnit(); Money cpcBidAmount = getBid((BiddableAdGroupCriterion) adGroupCriterion); if (cpcBidAmount != null) { childNode = childNode.setBid(cpcBidAmount.getMicroAmount()); } } else { childNode = childNode.asExcludedUnit(); } } addChildNodes(childNode, parentIdMap); } } /** * Returns the criterion-level bid, or null if no such bid exists. */ private static Money getBid(BiddableAdGroupCriterion biddableCriterion) { BiddingStrategyConfiguration biddingConfig = biddableCriterion.getBiddingStrategyConfiguration(); Money cpcBidAmount = null; if (biddingConfig.getBids() != null) { for (Bids bid : biddingConfig.getBids()) { if (bid instanceof CpcBid) { CpcBid cpcBid = (CpcBid) bid; if (BidSource.CRITERION.equals(cpcBid.getCpcBidSource())) { cpcBidAmount = cpcBid.getBid(); break; } } } } return cpcBidAmount; } }