diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java index ffd6c08d184..267ddd06afe 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java @@ -13,9 +13,12 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; +import org.prebid.server.auction.aliases.AlternateBidder; +import org.prebid.server.auction.aliases.AlternateBidderCodesConfig; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -25,6 +28,7 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.pubmatic.model.request.PubmaticBidderImpExt; import org.prebid.server.bidder.pubmatic.model.request.PubmaticExtDataAdServer; +import org.prebid.server.bidder.pubmatic.model.request.PubmaticMarketplace; import org.prebid.server.bidder.pubmatic.model.request.PubmaticWrapper; import org.prebid.server.bidder.pubmatic.model.response.PubmaticBidExt; import org.prebid.server.bidder.pubmatic.model.response.PubmaticBidResponse; @@ -59,6 +63,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -70,6 +75,7 @@ public class PubmaticBidder implements Bidder { private static final String IMP_EXT_AD_UNIT_KEY = "dfp_ad_unit_code"; private static final String AD_SERVER_GAM = "gam"; private static final String PREBID = "prebid"; + private static final String MARKETPLACE_EXT_REQUEST = "marketplace"; private static final String ACAT_EXT_REQUEST = "acat"; private static final String WRAPPER_EXT_REQUEST = "wrapper"; private static final String BIDDER_NAME = "pubmatic"; @@ -79,6 +85,8 @@ public class PubmaticBidder implements Bidder { private static final String IMP_EXT_ADSERVER = "adserver"; private static final List IMP_EXT_DATA_RESERVED_FIELD = List.of(IMP_EXT_PBADSLOT, IMP_EXT_ADSERVER); private static final String DCTR_VALUE_FORMAT = "%s=%s"; + private static final String WILDCARD = "*"; + private static final String WILDCARD_ALL = "all"; private final String endpointUrl; private final JacksonMapper mapper; @@ -97,10 +105,13 @@ public Result>> makeHttpRequests(BidRequest request PubmaticWrapper wrapper; final List acat; final Pair displayManagerFields; + final List allowedBidders; try { - acat = extractAcat(request); - wrapper = extractWrapper(request); + final JsonNode bidderparams = getExtRequestPrebidBidderparams(request); + acat = extractAcat(bidderparams); + wrapper = extractWrapper(bidderparams); + allowedBidders = extractAllowedBidders(request); displayManagerFields = extractDisplayManagerFields(request.getApp()); } catch (IllegalArgumentException e) { return Result.withError(BidderError.badInput(e.getMessage())); @@ -129,12 +140,43 @@ public Result>> makeHttpRequests(BidRequest request return Result.withErrors(errors); } - final BidRequest modifiedBidRequest = modifyBidRequest(request, validImps, publisherId, wrapper, acat); + final BidRequest modifiedBidRequest = modifyBidRequest( + request, validImps, publisherId, wrapper, acat, allowedBidders); return Result.of(Collections.singletonList(makeHttpRequest(modifiedBidRequest)), errors); } - private List extractAcat(BidRequest request) { - final JsonNode bidderParams = getExtRequestPrebidBidderparams(request); + private List extractAllowedBidders(BidRequest request) { + final AlternateBidderCodesConfig alternateBidderCodes = Optional.ofNullable(request.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAlternateBidderCodes) + .orElse(null); + + if (alternateBidderCodes == null) { + return null; + } + + if (BooleanUtils.isNotTrue(alternateBidderCodes.getEnabled())) { + return Collections.singletonList(BIDDER_NAME); + } + + final AlternateBidder alternateBidder = Optional.ofNullable(alternateBidderCodes.getBidders()) + .map(bidders -> bidders.get(BIDDER_NAME)) + .filter(bidder -> BooleanUtils.isTrue(bidder.getEnabled())) + .orElse(null); + + if (alternateBidder == null) { + return Collections.singletonList(BIDDER_NAME); + } + + final Set allowedBidderCodes = alternateBidder.getAllowedBidderCodes(); + if (allowedBidderCodes == null || allowedBidderCodes.contains(WILDCARD)) { + return Collections.singletonList(WILDCARD_ALL); + } + + return Stream.concat(Stream.of(BIDDER_NAME), allowedBidderCodes.stream()).toList(); + } + + private List extractAcat(JsonNode bidderParams) { final JsonNode acatNode = bidderParams != null ? bidderParams.get(ACAT_EXT_REQUEST) : null; return acatNode != null && acatNode.isArray() @@ -144,9 +186,8 @@ private List extractAcat(BidRequest request) { : null; } - private PubmaticWrapper extractWrapper(BidRequest request) { - final JsonNode pubmatic = getExtRequestPrebidBidderparams(request); - final JsonNode wrapperNode = pubmatic != null ? pubmatic.get(WRAPPER_EXT_REQUEST) : null; + private PubmaticWrapper extractWrapper(JsonNode bidderParams) { + final JsonNode wrapperNode = bidderParams != null ? bidderParams.get(WRAPPER_EXT_REQUEST) : null; return wrapperNode != null && wrapperNode.isObject() ? mapper.mapper().convertValue(wrapperNode, PubmaticWrapper.class) @@ -433,13 +474,14 @@ private BidRequest modifyBidRequest(BidRequest request, List imps, String publisherId, PubmaticWrapper wrapper, - List acat) { + List acat, + List allowedBidders) { return request.toBuilder() .imp(imps) .site(modifySite(request.getSite(), publisherId)) .app(modifyApp(request.getApp(), publisherId)) - .ext(modifyExtRequest(wrapper, acat)) + .ext(modifyExtRequest(wrapper, acat, allowedBidders)) .build(); } @@ -465,7 +507,7 @@ private static Publisher modifyPublisher(Publisher publisher, String publisherId : Publisher.builder().id(publisherId).build(); } - private ExtRequest modifyExtRequest(PubmaticWrapper wrapper, List acat) { + private ExtRequest modifyExtRequest(PubmaticWrapper wrapper, List acat, List allowedBidders) { final ObjectNode extNode = mapper.mapper().createObjectNode(); if (wrapper != null) { @@ -476,6 +518,10 @@ private ExtRequest modifyExtRequest(PubmaticWrapper wrapper, List acat) extNode.putPOJO(ACAT_EXT_REQUEST, acat); } + if (allowedBidders != null) { + extNode.putPOJO(MARKETPLACE_EXT_REQUEST, PubmaticMarketplace.of(allowedBidders)); + } + final ExtRequest newExtRequest = ExtRequest.empty(); return extNode.isEmpty() ? newExtRequest diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticMarketplace.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticMarketplace.java new file mode 100644 index 00000000000..8c4f593dbd3 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticMarketplace.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.pubmatic.model.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class PubmaticMarketplace { + + @JsonProperty("allowedbidders") + List allowedBidders; +} diff --git a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java b/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java index 054d2f2d13e..7eccaefb616 100644 --- a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java @@ -1,6 +1,8 @@ package org.prebid.server.bidder.pubmatic; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; @@ -17,6 +19,7 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.prebid.server.VertxTest; import org.prebid.server.bidder.model.BidderBid; @@ -38,6 +41,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAlternateBidderCodes; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAlternateBidderCodesBidder; import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmatic; import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmaticKeyVal; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; @@ -50,6 +55,7 @@ import java.math.BigDecimal; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.UnaryOperator; import static java.util.Arrays.asList; @@ -214,6 +220,175 @@ public void makeHttpRequestsShouldReturnBidRequestExtIfAcatFieldIsValidAndTrimWh .containsExactly(expectedExtRequest); } + @Test + public void makeHttpRequestsShouldReturnAllowedBidderCodeWithPubmaticAdded() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic", + ExtRequestPrebidAlternateBidderCodesBidder.of(true, Set.of("bidder1", "bidder2"))))) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("pubmatic", "bidder1", "bidder2"); + } + + @Test + public void makeHttpRequestsShouldReturnOnlyPubmaticWhenPubmaticCodesAreDisabled() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic", + ExtRequestPrebidAlternateBidderCodesBidder.of(false, Set.of("bidder1", "bidder2"))))) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("pubmatic"); + } + + @Test + public void makeHttpRequestsShouldReturnOnlyAllWhenPubmaticCodesAreAbsent() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic", + ExtRequestPrebidAlternateBidderCodesBidder.of(true, null)))) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("all"); + } + + @Test + public void makeHttpRequestsShouldReturnOnlyAllWhenPubmaticCodesHasWildcard() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic", + ExtRequestPrebidAlternateBidderCodesBidder.of(true, Set.of("*"))))) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("all"); + } + + @Test + public void makeHttpRequestsShouldReturnOnlyPubmaticWhenPubmaticCodesAreAbsent() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of())) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("pubmatic"); + } + + @Test + public void makeHttpRequestsShouldReturnOnlyPubmaticWhenAlternateBidderCodesAreDisabled() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(false, Map.of())) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("pubmatic"); + } + + @Test + public void makeHttpRequestsShouldReturnNothingWhenAlternateBidderCodeIsAbsent() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(null) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .isNull(); + } + @Test public void makeHttpRequestsShouldMergeWrappersFromImpAndBidRequestExt() { // given