From 61c012142706d1c759eae6c9962e3eccade7d3b2 Mon Sep 17 00:00:00 2001 From: pkaczmarek Date: Wed, 16 Apr 2025 14:37:34 +0200 Subject: [PATCH 1/3] EPlanning: Add schain support --- .../bidder/eplanning/EplanningBidder.java | 68 ++++++- .../bidder/eplanning/EplanningBidderTest.java | 175 ++++++++++++++++++ 2 files changed, 242 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java b/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java index aab1f5b7e86..a22a47a7b68 100644 --- a/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java +++ b/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java @@ -1,6 +1,7 @@ package org.prebid.server.bidder.eplanning; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; @@ -8,6 +9,9 @@ import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.request.SupplyChain; +import com.iab.openrtb.request.SupplyChainNode; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import io.vertx.core.MultiMap; @@ -15,6 +19,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; +import org.apache.logging.log4j.util.Strings; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.eplanning.model.CleanStepName; import org.prebid.server.bidder.eplanning.model.HbResponse; @@ -269,7 +274,18 @@ private String resolveRequestUri(BidRequest request, List requestsString uriBuilder.addParameter("app", REQUEST_TARGET_INVENTORY); } - return uriBuilder.toString(); + final Source source = request.getSource(); + if (source != null && source.getExt() != null && source.getExt().getSchain() != null) { + final SupplyChain supplyChain = source.getExt().getSchain(); + if (supplyChain.getNodes() != null && supplyChain.getNodes().size() <= 2) { + final String schValue = makeSupplyChain(supplyChain); + if (StringUtils.isNotEmpty(schValue)) { + uriBuilder.addParameter("sch", schValue); + } + } + } + + return uriBuilder.toString().replace("+", "%20"); } private static URL parseUrl(String url) { @@ -280,6 +296,56 @@ private static URL parseUrl(String url) { } } + private String makeSupplyChain(SupplyChain supplyChain) { + if (supplyChain.getNodes() == null || supplyChain.getNodes().isEmpty()) { + return Strings.EMPTY; + } + + final StringBuilder sb = new StringBuilder(); + sb.append(supplyChain.getVer()) + .append(",") + .append(supplyChain.getComplete() != null ? supplyChain.getComplete() : 0); + + for (SupplyChainNode node : supplyChain.getNodes()) { + sb.append("!") + .append(formatNodeValue(node.getAsi())) + .append(",") + .append(formatNodeValue(node.getSid())) + .append(",") + .append(formatNodeValue(node.getHp())) + .append(",") + .append(formatNodeValue(node.getRid())) + .append(",") + .append(formatNodeValue(node.getName())) + .append(",") + .append(formatNodeValue(node.getDomain())) + .append(",") + .append(formatNodeValue(node.getExt())); + } + + return sb.toString(); + } + + private String formatNodeValue(Object value) { + if (value == null) { + return Strings.EMPTY; + } + + if (value instanceof String) { + return (String) value; + } + + if (value instanceof Number) { + return value.toString(); + } + + if (value instanceof ObjectNode) { + return value.toString(); + } + + return Strings.EMPTY; + } + /** * Converts response to {@link List} of {@link BidderBid}s with {@link List} of errors. * Handles cases when response status is different to OK 200. diff --git a/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java b/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java index 00ba9278765..f6211591542 100644 --- a/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java @@ -8,6 +8,9 @@ import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.request.SupplyChain; +import com.iab.openrtb.request.SupplyChainNode; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import io.netty.handler.codec.http.HttpHeaderValues; @@ -25,6 +28,7 @@ import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; import org.prebid.server.proto.openrtb.ext.request.eplanning.ExtImpEplanning; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -35,6 +39,7 @@ import java.util.function.Function; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; @@ -413,6 +418,176 @@ public void makeHttpRequestsShouldSetCorrectUriWithApp() { + "appn=appName&appid=id&ifa=ifa&app=1"); } + @Test + public void makeHttpRequestsShouldNotAppendSchainIfSourceIsNull() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(null) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).doesNotContain("sch="); + } + + @Test + public void makeHttpRequestsShouldNotAppendSchainIfExtIsNull() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(Source.builder().ext(null).build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).doesNotContain("sch="); + } + + @Test + public void makeHttpRequestsShouldNotAppendSchainIfSchainIsNull() { + // given + final Source source = Source.builder() + .ext(ExtSource.of(null)) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(source) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).doesNotContain("sch="); + } + + @Test + public void makeHttpRequestsShouldNotAppendSchainIfNoNodes() { + // given + final SupplyChain supplyChain = SupplyChain.of(1, emptyList(), "1.0", null); + final Source source = Source.builder() + .ext(ExtSource.of(supplyChain)) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(source) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).doesNotContain("sch="); + } + + @Test + public void makeHttpRequestsShouldNotAppendSchainIfNodeCountIsGreaterThan2() { + // given + final List nodes = asList( + SupplyChainNode.of("asi1", "sid1", "rid1", "name1", "domain1", 1, null), + SupplyChainNode.of("asi2", "sid2", "rid2", "name2", "domain2", 1, null), + SupplyChainNode.of("asi3", "sid3", "rid3", "name3", "domain3", 1, null) + ); + final SupplyChain supplyChain = SupplyChain.of(1, nodes, "1.0", null); + final Source source = Source.builder() + .ext(ExtSource.of(supplyChain)) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(source) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).doesNotContain("sch="); + } + + @Test + public void makeHttpRequestsShouldAppendSchainForUpToTwoNodes() { + // given + final List nodes = asList( + SupplyChainNode.of("asi1", "sid1", "rid1", "name1", "domain1", 1, null), + SupplyChainNode.of("asi2", "sid2", null, null, "domain2", 1, null) + ); + final SupplyChain supplyChain = SupplyChain.of(1, nodes, "1.0", null); + final Source source = Source.builder() + .ext(ExtSource.of(supplyChain)) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(source) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri) + .contains("sch=") + .contains("%21asi1%2Csid1%2C1%2Crid1%2Cname1%2Cdomain1%2C") + .contains("%21asi2%2Csid2%2C1%2C%2C%2Cdomain2%2C"); + } + + @Test + public void makeHttpRequestsShouldUrlEncodeSchainFieldsCorrectly() { + // given + final List nodes = singletonList( + SupplyChainNode.of( + "a si", + "s/id", + null, + "r:id", + "na me", + 1, + jacksonMapper.mapper().createObjectNode().put("k", "v val")) + ); + final SupplyChain supplyChain = SupplyChain.of(0, nodes, "1.0", null); + final Source source = Source.builder() + .ext(ExtSource.of(supplyChain)) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(source) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final String uri = result.getValue().get(0).getUri(); + + assertThat(uri).contains("&sch="); + assertThat(uri).contains("1.0%2C0"); + assertThat(uri).contains("%21a%20si"); + assertThat(uri).contains("s%2Fid"); + assertThat(uri).contains("r%3Aid"); + assertThat(uri).contains("na%20me"); + assertThat(uri).contains("%7B%22k%22%3A%22v%20val%22%7D"); + } + @Test public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given From 17e049e5ecdd51114e82d5eb19fa60c02209b97d Mon Sep 17 00:00:00 2001 From: pkaczmarek Date: Wed, 23 Apr 2025 12:26:36 +0200 Subject: [PATCH 2/3] fix comments --- .../bidder/eplanning/EplanningBidder.java | 92 +++++++++---------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java b/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java index a22a47a7b68..354c5bdce54 100644 --- a/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java +++ b/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java @@ -1,7 +1,6 @@ package org.prebid.server.bidder.eplanning; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; @@ -17,9 +16,9 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; -import org.apache.logging.log4j.util.Strings; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.eplanning.model.CleanStepName; import org.prebid.server.bidder.eplanning.model.HbResponse; @@ -34,6 +33,7 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; import org.prebid.server.proto.openrtb.ext.request.eplanning.ExtImpEplanning; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -50,6 +50,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -274,15 +275,9 @@ private String resolveRequestUri(BidRequest request, List requestsString uriBuilder.addParameter("app", REQUEST_TARGET_INVENTORY); } - final Source source = request.getSource(); - if (source != null && source.getExt() != null && source.getExt().getSchain() != null) { - final SupplyChain supplyChain = source.getExt().getSchain(); - if (supplyChain.getNodes() != null && supplyChain.getNodes().size() <= 2) { - final String schValue = makeSupplyChain(supplyChain); - if (StringUtils.isNotEmpty(schValue)) { - uriBuilder.addParameter("sch", schValue); - } - } + final String schain = getSchainParameter(request.getSource()); + if (schain != null) { + uriBuilder.addParameter("sch", schain); } return uriBuilder.toString().replace("+", "%20"); @@ -296,54 +291,51 @@ private static URL parseUrl(String url) { } } - private String makeSupplyChain(SupplyChain supplyChain) { - if (supplyChain.getNodes() == null || supplyChain.getNodes().isEmpty()) { - return Strings.EMPTY; - } + private String getSchainParameter(Source source) { + return Optional.ofNullable(source) + .map(Source::getExt) + .map(ExtSource::getSchain) + .map(this::resolveSupplyChain) + .orElse(null); + } - final StringBuilder sb = new StringBuilder(); - sb.append(supplyChain.getVer()) - .append(",") - .append(supplyChain.getComplete() != null ? supplyChain.getComplete() : 0); - - for (SupplyChainNode node : supplyChain.getNodes()) { - sb.append("!") - .append(formatNodeValue(node.getAsi())) - .append(",") - .append(formatNodeValue(node.getSid())) - .append(",") - .append(formatNodeValue(node.getHp())) - .append(",") - .append(formatNodeValue(node.getRid())) - .append(",") - .append(formatNodeValue(node.getName())) - .append(",") - .append(formatNodeValue(node.getDomain())) - .append(",") - .append(formatNodeValue(node.getExt())); + private String resolveSupplyChain(SupplyChain schain) { + final List nodes = schain.getNodes(); + if (CollectionUtils.isEmpty(nodes) || nodes.size() > 2) { + return null; } - return sb.toString(); - } + final StringBuilder schainBuilder = new StringBuilder(); - private String formatNodeValue(Object value) { - if (value == null) { - return Strings.EMPTY; - } + schainBuilder.append(schain.getVer()); + schainBuilder.append(","); + schainBuilder.append(ObjectUtils.defaultIfNull(schain.getComplete(), 0)); + for (SupplyChainNode node : schain.getNodes()) { + schainBuilder.append("!"); + schainBuilder.append(node.getAsi() != null ? node.getAsi() : ""); + schainBuilder.append(","); - if (value instanceof String) { - return (String) value; - } + schainBuilder.append(node.getSid() != null ? node.getSid() : ""); + schainBuilder.append(","); - if (value instanceof Number) { - return value.toString(); - } + schainBuilder.append(node.getHp() == null ? StringUtils.EMPTY : node.getHp()); + schainBuilder.append(","); + + schainBuilder.append(node.getRid() != null ? node.getRid() : ""); + schainBuilder.append(","); + + schainBuilder.append(node.getName() != null ? node.getName() : ""); + schainBuilder.append(","); + + schainBuilder.append(node.getDomain() != null ? node.getDomain() : ""); + schainBuilder.append(","); - if (value instanceof ObjectNode) { - return value.toString(); + schainBuilder.append(node.getExt() == null + ? StringUtils.EMPTY + : mapper.encodeToString(node.getExt())); } - return Strings.EMPTY; + return schainBuilder.toString(); } /** From ccefa06bb6a227d3c0610bb8ec2524cce659738a Mon Sep 17 00:00:00 2001 From: pkaczmarek Date: Fri, 25 Apr 2025 09:55:03 +0200 Subject: [PATCH 3/3] fix URL encoding --- .../bidder/eplanning/EplanningBidder.java | 17 +++++++++-------- .../bidder/eplanning/EplanningBidderTest.java | 6 +++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java b/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java index 354c5bdce54..09c607d7451 100644 --- a/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java +++ b/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java @@ -275,12 +275,13 @@ private String resolveRequestUri(BidRequest request, List requestsString uriBuilder.addParameter("app", REQUEST_TARGET_INVENTORY); } - final String schain = getSchainParameter(request.getSource()); + String schain = getSchainParameter(request.getSource()); if (schain != null) { + schain = schain.replace(" ", "%20"); uriBuilder.addParameter("sch", schain); } - return uriBuilder.toString().replace("+", "%20"); + return uriBuilder.toString(); } private static URL parseUrl(String url) { @@ -312,22 +313,22 @@ private String resolveSupplyChain(SupplyChain schain) { schainBuilder.append(ObjectUtils.defaultIfNull(schain.getComplete(), 0)); for (SupplyChainNode node : schain.getNodes()) { schainBuilder.append("!"); - schainBuilder.append(node.getAsi() != null ? node.getAsi() : ""); + schainBuilder.append(StringUtils.defaultString(node.getAsi())); schainBuilder.append(","); - schainBuilder.append(node.getSid() != null ? node.getSid() : ""); + schainBuilder.append(StringUtils.defaultString(node.getSid())); schainBuilder.append(","); - schainBuilder.append(node.getHp() == null ? StringUtils.EMPTY : node.getHp()); + schainBuilder.append(node.getHp() != null ? node.getHp() : StringUtils.EMPTY); schainBuilder.append(","); - schainBuilder.append(node.getRid() != null ? node.getRid() : ""); + schainBuilder.append(StringUtils.defaultString(node.getRid())); schainBuilder.append(","); - schainBuilder.append(node.getName() != null ? node.getName() : ""); + schainBuilder.append(StringUtils.defaultString(node.getName())); schainBuilder.append(","); - schainBuilder.append(node.getDomain() != null ? node.getDomain() : ""); + schainBuilder.append(StringUtils.defaultString(node.getDomain())); schainBuilder.append(","); schainBuilder.append(node.getExt() == null diff --git a/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java b/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java index f6211591542..8b94f14c737 100644 --- a/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java @@ -581,11 +581,11 @@ public void makeHttpRequestsShouldUrlEncodeSchainFieldsCorrectly() { assertThat(uri).contains("&sch="); assertThat(uri).contains("1.0%2C0"); - assertThat(uri).contains("%21a%20si"); + assertThat(uri).contains("%21a%2520si"); assertThat(uri).contains("s%2Fid"); assertThat(uri).contains("r%3Aid"); - assertThat(uri).contains("na%20me"); - assertThat(uri).contains("%7B%22k%22%3A%22v%20val%22%7D"); + assertThat(uri).contains("na%2520me"); + assertThat(uri).contains("%7B%22k%22%3A%22v%2520val%22%7D"); } @Test