Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
220 changes: 220 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,220 @@
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;

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<>();
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);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rewriteImpExt -> updateImp, rewrittenImp -> updatedImp

impsByFeed.computeIfAbsent(feedId, k -> new ArrayList<>()).add(rewrittenImp);
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}

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 on lines +87 to +89
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this checks. At this stage, neither the wrapper nor the wrapper.getBidder() can be null

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 on lines +98 to +110
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        final ObjectNode newExt = mapper.mapper().createObjectNode();
        newExt.put("feedId", feedId);

        final ObjectNode bidderNode = rewritten.putObject("bidder");

        final String placementId = ext.getPlacementId();
        if (StringUtils.isNotBlank(placementId)) {
            bidderNode.put("placementId", placementId);
        }

        final String publisherId = ext.getPublisherId();
        if (StringUtils.isNotBlank(publisherId)) {
            bidderNode.put("publisherId", publisherId);
        }

        return imp.toBuilder().ext(newExt).build();

}

private HttpRequest<BidRequest> buildHttpRequest(BidRequest request, String feedId) {
final String uri = endpointUrl + "?feed=" + URLEncoder.encode(feedId, StandardCharsets.UTF_8);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use HttpUtil.encodeUrl instead


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

Comment on lines +117 to +118
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to set Accept header. It's already included in HttpUtil.headers()

return HttpRequest.<BidRequest>builder()
.method(HttpMethod.POST)
.uri(uri)
.headers(headers)
.body(mapper.encodeToBytes(request))
.impIds(collectImpIds(request))
.payload(request)
.build();
Comment on lines +119 to +126
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use BidderUtil.defaultRequest instead

}

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();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List.of -> Collections.emptyList

}
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:
}
}

final JsonNode ext = bid.getExt();
Comment on lines +165 to +176
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor this method:

    private static BidType resolveBidType(Bid bid, List<Imp> imps) {
        return bidTypeFromMtype(bid.getMtype())
                .or(() -> bidTypeFromExt(bid.getExt()))
                .or(() -> bidTypeFromAdm(bid.getAdm()))
                .or(() -> bidTypeFromImp(bid.getImpid(), imps))
                .orElseThrow(() -> new PreBidException(
                        "Cannot determine media type for bid %s on imp %s".formatted(bid.getId(), bid.getImpid())));
    }

    private static Optional<BidType> bidTypeFromMtype(Integer mType) {
        return Optional.ofNullable(switch (mType) {
            case 1 -> BidType.banner;
            case 2 -> BidType.video;
            case null, default -> null;
        });
    }

    private static Optional<BidType> bidTypeFromExt(ObjectNode bidExt) {
        return Optional.ofNullable(bidExt)
                .map(ext -> ext.get("mediaType"))
                .filter(JsonNode::isTextual)
                .map(JsonNode::asText)
                .map(String::toLowerCase)
                .map(mediaType -> switch (mediaType) {
                    case "banner" -> BidType.banner;
                    case "video" -> BidType.video;
                    default -> null;
                });
    }

    private static Optional<BidType> bidTypeFromAdm(String adm) {
        if (StringUtils.isBlank(adm)) {
            return Optional.empty();
        }

        final String trimmed = adm.trim().toUpperCase();
        return trimmed.startsWith("<VAST") || trimmed.startsWith("<?XML")
                ? Optional.of(BidType.video)
                : Optional.empty();
    }

    private static Optional<BidType> bidTypeFromImp(String impId, List<Imp> imps) {
        for (Imp imp : imps) {
            if (!Objects.equals(imp.getId(), impId)) {
                continue;
            }

            final boolean hasBanner = imp.getBanner() != null;
            final boolean hasVideo = imp.getVideo() != null;
            if (hasVideo && !hasBanner) {
                return Optional.of(BidType.video);
            }
            if (hasBanner) {
                return Optional.of(BidType.banner);
            }

            break;
        }

        return Optional.empty();
    }

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) {
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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for @Validated

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