-
Notifications
You must be signed in to change notification settings - Fork 252
New Adapter: Adswag #4597
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
New Adapter: Adswag #4597
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
331 changes: 331 additions & 0 deletions
331
src/main/java/org/prebid/server/bidder/adswag/AdswagBidder.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,331 @@ | ||
| package org.prebid.server.bidder.adswag; | ||
|
|
||
| import com.fasterxml.jackson.core.type.TypeReference; | ||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| 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; | ||
| import com.iab.openrtb.request.Format; | ||
| import com.iab.openrtb.request.Imp; | ||
| import com.iab.openrtb.request.Publisher; | ||
| import com.iab.openrtb.request.Site; | ||
| import com.iab.openrtb.request.Video; | ||
| import com.iab.openrtb.response.Bid; | ||
| import com.iab.openrtb.response.BidResponse; | ||
| import com.iab.openrtb.response.SeatBid; | ||
| import org.apache.commons.collections4.CollectionUtils; | ||
| import org.apache.commons.lang3.StringUtils; | ||
| import org.prebid.server.bidder.Bidder; | ||
| import org.prebid.server.bidder.model.BidderBid; | ||
| import org.prebid.server.bidder.model.BidderCall; | ||
| import org.prebid.server.bidder.model.BidderError; | ||
| import org.prebid.server.bidder.model.HttpRequest; | ||
| import org.prebid.server.bidder.model.Result; | ||
| import org.prebid.server.exception.PreBidException; | ||
| 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.adswag.ExtImpAdswag; | ||
| import org.prebid.server.proto.openrtb.ext.response.BidType; | ||
| import org.prebid.server.util.BidderUtil; | ||
| import org.prebid.server.util.HttpUtil; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| public class AdswagBidder implements Bidder<BidRequest> { | ||
|
|
||
| private static final TypeReference<ExtPrebid<?, ExtImpAdswag>> TYPE_REFERENCE = new TypeReference<>() { | ||
| }; | ||
|
|
||
| private static final String DEFAULT_CURRENCY = "EUR"; | ||
| private static final Pattern VAST_MARKUP = Pattern.compile("<\\s*VAST[\\s/>]", Pattern.CASE_INSENSITIVE); | ||
|
|
||
| private final String endpointUrl; | ||
| private final JacksonMapper mapper; | ||
|
|
||
| public AdswagBidder(String endpointUrl, JacksonMapper mapper) { | ||
| this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); | ||
| this.mapper = Objects.requireNonNull(mapper); | ||
| } | ||
|
|
||
| @Override | ||
| public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) { | ||
| final Map<String, List<Imp>> impsByPublisher = new LinkedHashMap<>(); | ||
| final List<BidderError> errors = new ArrayList<>(); | ||
|
|
||
| for (Imp imp : request.getImp()) { | ||
| final ExtImpAdswag extImpAdswag; | ||
| try { | ||
| extImpAdswag = parseImpExt(imp); | ||
| validateImpExt(extImpAdswag, imp.getId()); | ||
| } catch (PreBidException e) { | ||
| errors.add(BidderError.badInput(e.getMessage())); | ||
| continue; | ||
| } | ||
|
|
||
| impsByPublisher | ||
| .computeIfAbsent(extImpAdswag.getPublisherId(), _ -> new ArrayList<>()) | ||
| .add(modifyImp(imp, extImpAdswag.getPlacementId())); | ||
| } | ||
|
|
||
| if (impsByPublisher.isEmpty()) { | ||
| return Result.withErrors(errors); | ||
| } | ||
|
|
||
| if (request.getSite() == null && request.getApp() == null) { | ||
| errors.add(BidderError.badInput("request must contain either site or app")); | ||
| return Result.withErrors(errors); | ||
| } | ||
|
|
||
| final List<HttpRequest<BidRequest>> httpRequests = impsByPublisher.entrySet().stream() | ||
| .map(group -> modifyBidRequest(request, group.getKey(), group.getValue())) | ||
| .map(modifiedRequest -> BidderUtil.defaultRequest(modifiedRequest, endpointUrl, mapper)) | ||
| .toList(); | ||
|
|
||
| return Result.of(httpRequests, errors); | ||
| } | ||
|
|
||
| private ExtImpAdswag parseImpExt(Imp imp) { | ||
| try { | ||
| return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new PreBidException("Error parsing imp.ext for impression " + imp.getId()); | ||
| } | ||
| } | ||
|
|
||
| private static void validateImpExt(ExtImpAdswag extImpAdswag, String impId) { | ||
| if (StringUtils.isBlank(extImpAdswag.getPublisherId())) { | ||
| throw new PreBidException("missing publisherId for imp " + impId); | ||
| } | ||
| } | ||
|
|
||
| private static Imp modifyImp(Imp imp, String placementId) { | ||
| final ObjectNode modifiedExt = imp.getExt().deepCopy(); | ||
| modifiedExt.remove("bidder"); | ||
| modifiedExt.remove("prebid"); | ||
| if (StringUtils.isNotBlank(placementId)) { | ||
| modifiedExt.putObject("adswag").put("placement_id", placementId); | ||
| } | ||
|
|
||
| return imp.toBuilder().ext(modifiedExt.isEmpty() ? null : modifiedExt).build(); | ||
| } | ||
|
|
||
| private static BidRequest modifyBidRequest(BidRequest request, String publisherId, List<Imp> modifiedImps) { | ||
| return request.toBuilder() | ||
| .site(modifySite(request.getSite(), publisherId)) | ||
| .app(modifyApp(request.getApp(), publisherId)) | ||
| .imp(modifiedImps) | ||
| .build(); | ||
| } | ||
|
|
||
| private static Site modifySite(Site site, String publisherId) { | ||
| return site != null | ||
| ? site.toBuilder().publisher(modifyPublisher(site.getPublisher(), publisherId)).build() | ||
| : null; | ||
| } | ||
|
|
||
| private static App modifyApp(App app, String publisherId) { | ||
| return app != null | ||
| ? app.toBuilder().publisher(modifyPublisher(app.getPublisher(), publisherId)).build() | ||
| : null; | ||
| } | ||
|
|
||
| private static Publisher modifyPublisher(Publisher publisher, String publisherId) { | ||
| return Optional.ofNullable(publisher) | ||
| .map(Publisher::toBuilder) | ||
| .orElseGet(Publisher::builder) | ||
| .id(publisherId) | ||
| .build(); | ||
| } | ||
|
|
||
| @Override | ||
| public final Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) { | ||
| final String body = httpCall.getResponse().getBody(); | ||
| if (StringUtils.isBlank(body)) { | ||
| return Result.empty(); | ||
| } | ||
|
|
||
| try { | ||
| final BidResponse bidResponse = mapper.decodeValue(body, BidResponse.class); | ||
| final List<BidderError> errors = new ArrayList<>(); | ||
| final List<BidderBid> bids = extractBids(httpCall.getRequest().getPayload(), bidResponse, errors); | ||
| return Result.of(bids, errors); | ||
| } catch (DecodeException e) { | ||
| return Result.withError(BidderError.badServerResponse(e.getMessage())); | ||
| } | ||
| } | ||
|
|
||
| private List<BidderBid> extractBids(BidRequest bidRequest, BidResponse bidResponse, List<BidderError> errors) { | ||
| if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { | ||
| return Collections.emptyList(); | ||
| } | ||
|
|
||
| final String currency = StringUtils.defaultIfBlank(bidResponse.getCur(), DEFAULT_CURRENCY); | ||
| return bidResponse.getSeatbid().stream() | ||
| .filter(Objects::nonNull) | ||
| .map(SeatBid::getBid) | ||
| .filter(Objects::nonNull) | ||
| .flatMap(List::stream) | ||
| .filter(Objects::nonNull) | ||
| .map(bid -> makeBidderBid(bid, bidRequest.getImp(), currency, errors)) | ||
| .filter(Objects::nonNull) | ||
| .toList(); | ||
| } | ||
|
|
||
| private BidderBid makeBidderBid(Bid bid, List<Imp> imps, String currency, List<BidderError> errors) { | ||
| final Imp imp = findImp(imps, bid.getImpid()); | ||
| if (imp == null) { | ||
| errors.add(BidderError.badServerResponse( | ||
| "bid %s references unknown imp %s".formatted(bid.getId(), bid.getImpid()))); | ||
| return null; | ||
| } | ||
|
|
||
| final String serveUrl = serveUrl(bid); | ||
| if (StringUtils.isBlank(bid.getAdm()) && StringUtils.isBlank(serveUrl)) { | ||
| errors.add(BidderError.badServerResponse( | ||
| "bid %s has neither adm nor ext.adswag.serve_url".formatted(bid.getId()))); | ||
| return null; | ||
| } | ||
|
|
||
| final BidType bidType; | ||
| try { | ||
| bidType = resolveBidType(imp, bid); | ||
| } catch (PreBidException e) { | ||
| errors.add(BidderError.badServerResponse(e.getMessage())); | ||
| return null; | ||
| } | ||
|
|
||
| final Bid modifiedBid = switch (bidType) { | ||
| case banner -> makeBannerBid(bid, serveUrl, imp.getBanner()); | ||
| case video -> makeVideoBid(bid, serveUrl, imp.getVideo()); | ||
| case audio -> makeAudioBid(bid, serveUrl); | ||
| // should never happen | ||
| default -> throw new AssertionError(); | ||
| }; | ||
|
|
||
| return BidderBid.of(modifiedBid, bidType, currency); | ||
| } | ||
|
|
||
| private static Imp findImp(List<Imp> imps, String impId) { | ||
| return imps.stream() | ||
| .filter(imp -> Objects.equals(imp.getId(), impId)) | ||
| .findFirst() | ||
| .orElse(null); | ||
| } | ||
|
|
||
| private static String serveUrl(Bid bid) { | ||
| final JsonNode serveUrlNode = bid.getExt() != null | ||
| ? bid.getExt().path("adswag").path("serve_url") | ||
| : null; | ||
| return serveUrlNode != null && serveUrlNode.isTextual() ? serveUrlNode.textValue() : null; | ||
| } | ||
|
|
||
| private static BidType resolveBidType(Imp imp, Bid bid) { | ||
| final Integer mtype = bid.getMtype(); | ||
| if (mtype != null) { | ||
| return switch (mtype) { | ||
| case 1 -> BidType.banner; | ||
| case 2 -> BidType.video; | ||
| case 3 -> BidType.audio; | ||
| default -> throw new PreBidException( | ||
| "unsupported bid.mtype %d for impression %s".formatted(mtype, bid.getImpid())); | ||
| }; | ||
| } | ||
|
|
||
| final boolean isVast = bid.getAdm() != null && VAST_MARKUP.matcher(bid.getAdm()).find(); | ||
| if (imp.getAudio() != null && (isVast || imp.getBanner() == null)) { | ||
| return BidType.audio; | ||
| } | ||
| if (imp.getVideo() != null && (isVast || imp.getBanner() == null)) { | ||
| return BidType.video; | ||
| } | ||
| if (imp.getBanner() != null) { | ||
| return BidType.banner; | ||
| } | ||
| throw new PreBidException("unable to resolve media type for impression " + bid.getImpid()); | ||
| } | ||
|
|
||
| private static Bid makeBannerBid(Bid bid, String serveUrl, Banner banner) { | ||
| final Format size = bannerSize(banner); | ||
| final boolean updateSize = hasNoSize(bid) && size != null; | ||
|
|
||
| return bid.toBuilder() | ||
| .adm(StringUtils.isBlank(bid.getAdm()) ? iframeMarkup(serveUrl, size) : bid.getAdm()) | ||
| .w(updateSize ? size.getW() : bid.getW()) | ||
| .h(updateSize ? size.getH() : bid.getH()) | ||
| .mtype(1) | ||
| .build(); | ||
| } | ||
|
|
||
| private static Format bannerSize(Banner banner) { | ||
| if (banner == null) { | ||
| return null; | ||
| } | ||
| if (CollectionUtils.isNotEmpty(banner.getFormat())) { | ||
| return banner.getFormat().getFirst(); | ||
| } | ||
| if (banner.getW() != null && banner.getH() != null) { | ||
| return Format.builder().w(banner.getW()).h(banner.getH()).build(); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private static String iframeMarkup(String serveUrl, Format size) { | ||
| final StringBuilder markup = new StringBuilder() | ||
| .append("<iframe src=\"").append(escapeHtmlAttribute(serveUrl)).append("\" "); | ||
|
|
||
| if (size != null) { | ||
| markup | ||
| .append("width=\"").append(size.getW()).append("\" ") | ||
| .append("height=\"").append(size.getH()).append("\" "); | ||
| } | ||
| markup.append(""" | ||
| frameborder="0" \ | ||
| scrolling="no" \ | ||
| marginheight="0" \ | ||
| marginwidth="0" \ | ||
| style="border:0" \ | ||
| title="Advertisement"></iframe>"""); | ||
|
|
||
| return markup.toString(); | ||
| } | ||
|
|
||
| // Mirrors Go's html.EscapeString so both server adapters emit identical markup. | ||
| private static String escapeHtmlAttribute(String value) { | ||
| return value | ||
| .replace("&", "&") | ||
| .replace("'", "'") | ||
| .replace("<", "<") | ||
| .replace(">", ">") | ||
| .replace("\"", """); | ||
| } | ||
|
|
||
| private static boolean hasNoSize(Bid bid) { | ||
| return bid.getW() == null || bid.getW() <= 0 || bid.getH() == null || bid.getH() <= 0; | ||
| } | ||
|
|
||
| private static Bid makeVideoBid(Bid bid, String serveUrl, Video video) { | ||
| final boolean updateSize = hasNoSize(bid) && video != null && video.getW() != null && video.getH() != null; | ||
| return bid.toBuilder() | ||
| .nurl(StringUtils.isBlank(bid.getAdm()) ? serveUrl : bid.getNurl()) | ||
| .w(updateSize ? video.getW() : bid.getW()) | ||
| .h(updateSize ? video.getH() : bid.getH()) | ||
| .mtype(2) | ||
| .build(); | ||
| } | ||
|
|
||
| private static Bid makeAudioBid(Bid bid, String serveUrl) { | ||
| return bid.toBuilder() | ||
| .nurl(StringUtils.isBlank(bid.getAdm()) ? serveUrl : bid.getNurl()) | ||
| .mtype(3) | ||
| .build(); | ||
| } | ||
| } | ||
14 changes: 14 additions & 0 deletions
14
src/main/java/org/prebid/server/proto/openrtb/ext/request/adswag/ExtImpAdswag.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package org.prebid.server.proto.openrtb.ext.request.adswag; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import lombok.Value; | ||
|
|
||
| @Value(staticConstructor = "of") | ||
| public class ExtImpAdswag { | ||
|
|
||
| @JsonProperty("publisherId") | ||
| String publisherId; | ||
|
|
||
| @JsonProperty("placementId") | ||
| String placementId; | ||
| } |
35 changes: 35 additions & 0 deletions
35
src/main/java/org/prebid/server/spring/config/bidder/AdswagConfiguration.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package org.prebid.server.spring.config.bidder; | ||
|
|
||
| import org.prebid.server.bidder.BidderDeps; | ||
| import org.prebid.server.bidder.adswag.AdswagBidder; | ||
| import org.prebid.server.json.JacksonMapper; | ||
| import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; | ||
| import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; | ||
| import org.prebid.server.spring.env.YamlPropertySourceFactory; | ||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.context.annotation.PropertySource; | ||
|
|
||
| @Configuration | ||
| @PropertySource(value = "classpath:/bidder-config/adswag.yaml", factory = YamlPropertySourceFactory.class) | ||
| public class AdswagConfiguration { | ||
|
|
||
| private static final String BIDDER_NAME = "adswag"; | ||
|
|
||
| @Bean("adswagConfigurationProperties") | ||
| @ConfigurationProperties("adapters.adswag") | ||
| BidderConfigurationProperties configurationProperties() { | ||
| return new BidderConfigurationProperties(); | ||
| } | ||
|
|
||
| @Bean | ||
| BidderDeps adswagBidderDeps(BidderConfigurationProperties adswagConfigurationProperties, | ||
| JacksonMapper mapper) { | ||
|
|
||
| return BidderDepsAssembler.forBidder(BIDDER_NAME) | ||
| .withConfig(adswagConfigurationProperties) | ||
| .bidderCreator(config -> new AdswagBidder(config.getEndpoint(), mapper)) | ||
| .assemble(); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for this check. Inline
bodyThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kept this one, with apologies: the Adswag endpoint answers a no-bid on this path as an empty-body HTTP 200 (alongside the conventional 204), and core only substitutes a safe body for 204 (HttpBidderRequester#toHttpCallWithSafeResponseBody), so removing the check would surface every no-bid as a bad_server_response decode error. Happy to drop it if you'd prefer we require 204-only no-bids from the endpoint instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@westerschmal It's safe to remove, because
mapper.decodeValue(body, BidResponse.class);will returnnulland you have