Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions src/main/java/org/prebid/server/bidder/revantage/RevantageBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package org.prebid.server.bidder.revantage;

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.BidRequest;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpMethod;
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.revantage.ExtImpRevantage;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.HttpUtil;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
* Prebid Server adapter for Revantage SSP.
*
* <p>Mirrors the Prebid.js client adapter (revantageBidAdapter.js): impressions
* are grouped by feedId and each feed becomes a separate POST to
* {@code https://bid.revantage.io/bid?feed=<feedId>}. The imp.ext payload is
* rewritten to the shape expected by the upstream endpoint:
*
* <pre>
* {
* "feedId": "...",
* "bidder": {
* "placementId": "...", // optional
* "publisherId": "..." // optional
* }
* }
* </pre>
*/
Comment thread
v0idxyz marked this conversation as resolved.
Outdated
public class RevantageBidder implements Bidder<BidRequest> {

private static final TypeReference<ExtPrebid<?, ExtImpRevantage>> REVANTAGE_EXT_TYPE_REFERENCE =
new TypeReference<>() {};

private static final String DEFAULT_CURRENCY = "USD";

private final String endpointUrl;
private final JacksonMapper mapper;

public RevantageBidder(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 List<BidderError> errors = new ArrayList<>();
// LinkedHashMap preserves first-seen feedId order for deterministic output.
Comment thread
v0idxyz marked this conversation as resolved.
Outdated
final Map<String, List<Imp>> impsByFeed = new LinkedHashMap<>();

for (Imp imp : request.getImp()) {
try {
final ExtImpRevantage ext = parseImpExt(imp);
final String feedId = StringUtils.trimToNull(ext.getFeedId());
if (feedId == null) {
throw new PreBidException("imp %s: missing required param feedId".formatted(imp.getId()));
}
final Imp rewrittenImp = rewriteImpExt(imp, feedId, ext);
Comment thread
v0idxyz marked this conversation as resolved.
Outdated
impsByFeed.computeIfAbsent(feedId, k -> new ArrayList<>()).add(rewrittenImp);
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}
Comment thread
v0idxyz marked this conversation as resolved.

if (impsByFeed.isEmpty()) {
return Result.withErrors(errors);
}

final List<HttpRequest<BidRequest>> requests = new ArrayList<>(impsByFeed.size());
for (Map.Entry<String, List<Imp>> entry : impsByFeed.entrySet()) {
final BidRequest outgoing = request.toBuilder().imp(entry.getValue()).build();
requests.add(buildHttpRequest(outgoing, entry.getKey()));
}
return Result.of(requests, errors);
}

private ExtImpRevantage parseImpExt(Imp imp) {
try {
final ExtPrebid<?, ExtImpRevantage> wrapper =
mapper.mapper().convertValue(imp.getExt(), REVANTAGE_EXT_TYPE_REFERENCE);
if (wrapper == null || wrapper.getBidder() == null) {
throw new PreBidException("imp %s: missing imp.ext.bidder".formatted(imp.getId()));
}
Comment thread
v0idxyz marked this conversation as resolved.
Outdated
return wrapper.getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException(
"imp %s: invalid imp.ext: %s".formatted(imp.getId(), e.getMessage()));
}
}

private Imp rewriteImpExt(Imp imp, String feedId, ExtImpRevantage ext) {
final ObjectNode bidderNode = mapper.mapper().createObjectNode();
if (StringUtils.isNotBlank(ext.getPlacementId())) {
bidderNode.put("placementId", ext.getPlacementId());
}
if (StringUtils.isNotBlank(ext.getPublisherId())) {
bidderNode.put("publisherId", ext.getPublisherId());
}

final ObjectNode rewritten = mapper.mapper().createObjectNode();
rewritten.put("feedId", feedId);
rewritten.set("bidder", bidderNode);

return imp.toBuilder().ext(rewritten).build();
Comment thread
v0idxyz marked this conversation as resolved.
Outdated
}

private HttpRequest<BidRequest> buildHttpRequest(BidRequest request, String feedId) {
final String uri = endpointUrl + "?feed=" + URLEncoder.encode(feedId, StandardCharsets.UTF_8);
Comment thread
v0idxyz marked this conversation as resolved.
Outdated

final MultiMap headers = HttpUtil.headers();
headers.set(HttpUtil.ACCEPT_HEADER, "application/json");

Comment thread
v0idxyz marked this conversation as resolved.
Outdated
return HttpRequest.<BidRequest>builder()
.method(HttpMethod.POST)
.uri(uri)
.headers(headers)
.body(mapper.encodeToBytes(request))
.impIds(collectImpIds(request))
.payload(request)
.build();
Comment thread
v0idxyz marked this conversation as resolved.
Outdated
}

private static List<String> collectImpIds(BidRequest request) {
final List<String> ids = new ArrayList<>(request.getImp().size());
for (Imp imp : request.getImp()) {
ids.add(imp.getId());
}
return ids;
}

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
try {
final BidResponse response = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
return Result.withValues(extractBids(response, bidRequest));
} catch (DecodeException | PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

private static List<BidderBid> extractBids(BidResponse response, BidRequest request) {
if (response == null || CollectionUtils.isEmpty(response.getSeatbid())) {
return List.of();
Comment thread
v0idxyz marked this conversation as resolved.
Outdated
}
final String currency = StringUtils.defaultIfBlank(response.getCur(), DEFAULT_CURRENCY);
final List<BidderBid> bids = new ArrayList<>();
for (SeatBid seatBid : response.getSeatbid()) {
if (seatBid == null || CollectionUtils.isEmpty(seatBid.getBid())) {
continue;
}
for (Bid bid : seatBid.getBid()) {
final BidType type = resolveMediaType(bid, request.getImp());
bids.add(BidderBid.of(bid, type, seatBid.getSeat(), currency));
}
}
return bids;
}

private static BidType resolveMediaType(Bid bid, List<Imp> imps) {
if (bid.getMtype() != null) {
switch (bid.getMtype()) {
case 1:
return BidType.banner;
case 2:
return BidType.video;
default:
// fall through
}
}

final JsonNode ext = bid.getExt();
Comment thread
v0idxyz marked this conversation as resolved.
Outdated
if (ext != null) {
final JsonNode mediaTypeNode = ext.get("mediaType");
if (mediaTypeNode != null && mediaTypeNode.isTextual()) {
final String value = mediaTypeNode.asText().toLowerCase();
if ("banner".equals(value)) {
return BidType.banner;
}
if ("video".equals(value)) {
return BidType.video;
}
}
}

if (isVastMarkup(bid.getAdm())) {
return BidType.video;
}

for (Imp imp : imps) {
if (!Objects.equals(imp.getId(), bid.getImpid())) {
continue;
}
final boolean hasBanner = imp.getBanner() != null;
final boolean hasVideo = imp.getVideo() != null;
if (hasVideo && !hasBanner) {
return BidType.video;
}
if (hasBanner) {
// banner-only or multi-format with no signal — default to banner
return BidType.banner;
}
break;
}

throw new PreBidException(
"Cannot determine media type for bid %s on imp %s".formatted(bid.getId(), bid.getImpid()));
}

private static boolean isVastMarkup(String adm) {
if (StringUtils.isBlank(adm)) {
return false;
}
final String trimmed = adm.trim().toUpperCase();
return trimmed.startsWith("<VAST") || trimmed.startsWith("<?XML");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.prebid.server.proto.openrtb.ext.request.revantage;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

/**
* Bidder-specific portion of imp.ext.bidder for the Revantage adapter.
*
* <p>{@code feedId} is required. {@code placementId} and {@code publisherId}
* are optional pass-through identifiers.
*/
Comment thread
v0idxyz marked this conversation as resolved.
@Value(staticConstructor = "of")
public class ExtImpRevantage {

@JsonProperty("feedId")
String feedId;

@JsonProperty("placementId")
String placementId;

@JsonProperty("publisherId")
String publisherId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.prebid.server.spring.config.bidder;

import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.revantage.RevantageBidder;
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.config.bidder.util.UsersyncerCreator;
import org.prebid.server.spring.env.YamlPropertySourceFactory;
import org.springframework.beans.factory.annotation.Value;
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;
import org.springframework.validation.annotation.Validated;

import jakarta.validation.constraints.NotBlank;

@Configuration
@PropertySource(value = "classpath:/bidder-config/revantage.yaml", factory = YamlPropertySourceFactory.class)
public class RevantageConfiguration {

private static final String BIDDER_NAME = "revantage";

@Bean("revantageConfigurationProperties")
@ConfigurationProperties("adapters.revantage")
@Validated
Comment thread
v0idxyz marked this conversation as resolved.
Outdated
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

@Bean
BidderDeps revantageBidderDeps(
BidderConfigurationProperties revantageConfigurationProperties,
@NotBlank @Value("${external-url}") String externalUrl,
JacksonMapper mapper) {

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(revantageConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new RevantageBidder(config.getEndpoint(), mapper))
.assemble();
}
}
19 changes: 19 additions & 0 deletions src/main/resources/bidder-config/revantage.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
adapters:
revantage:
endpoint: https://bid.revantage.io/bid
geoscope:
- GLOBAL
meta-info:
maintainer-email: prebid@revantage.io
app-media-types:
- banner
- video
site-media-types:
- banner
- video
supported-vendors:
vendor-id: 0
usersync:
redirect:
url: "https://sync.revantage.io/pbs/usersync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&r={{redirect_url}}"
uid-macro: "$UID"
22 changes: 22 additions & 0 deletions src/main/resources/static/bidder-params/revantage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Revantage Adapter Params",
"description": "A schema which validates params accepted by the Revantage adapter",
"type": "object",
"properties": {
"feedId": {
"type": "string",
"description": "Revantage feed identifier (required)",
"minLength": 1
},
"placementId": {
"type": "string",
"description": "Optional placement identifier"
},
"publisherId": {
"type": "string",
"description": "Optional publisher identifier"
}
},
"required": ["feedId"]
}
Loading