Skip to content

Commit

Permalink
Operaads: support multiformat request (#1709)
Browse files Browse the repository at this point in the history
  • Loading branch information
marki1an authored Feb 21, 2022
1 parent de135e7 commit ed4509f
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 110 deletions.
169 changes: 108 additions & 61 deletions src/main/java/org/prebid/server/bidder/operaads/OperaadsBidder.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.iab.openrtb.request.Banner;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Device;
Expand All @@ -27,10 +28,12 @@
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
import org.prebid.server.proto.openrtb.ext.request.operaads.ExtImpOperaads;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.BidderUtil;
import org.prebid.server.util.HttpUtil;
import org.prebid.server.util.ObjectUtil;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
Expand All @@ -44,6 +47,7 @@ public class OperaadsBidder implements Bidder<BidRequest> {
};
private static final String PUBLISHER_ID_MACRO = "{{PublisherId}}";
private static final String ACCOUNT_ID_MACRO = "{{AccountId}}";
private static final String BIDDER_CURRENCY = "USD";

private final String endpointUrl;
private final JacksonMapper mapper;
Expand All @@ -56,7 +60,7 @@ public OperaadsBidder(String endpointUrl, JacksonMapper mapper) {
@Override
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
try {
checkRequest(request);
validateDevice(request.getDevice());
} catch (PreBidException e) {
return Result.withError(BidderError.badInput(e.getMessage()));
}
Expand All @@ -65,21 +69,25 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request
final List<BidderError> errors = new ArrayList<>();

for (Imp imp : request.getImp()) {
final ExtImpOperaads extImpOperaads;
final Imp modifiedImp;
try {
final ExtImpOperaads extImpOperaads = parseImpExt(imp);
final Imp resolvedImp = resolveImp(imp, extImpOperaads);

requests.add(createRequest(request, resolvedImp, extImpOperaads));
extImpOperaads = parseImpExt(imp);
modifiedImp = modifyImp(imp, extImpOperaads.getPlacementId());
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
continue;
}

if (modifiedImp != null) {
requests.add(createRequest(request, modifiedImp, extImpOperaads));
}
}

return Result.of(requests, errors);
}

private static void checkRequest(BidRequest request) {
final Device device = request.getDevice();
private static void validateDevice(Device device) {
if (device == null || StringUtils.isEmpty(device.getOs())) {
throw new PreBidException("Request is missing device OS information");
}
Expand All @@ -89,41 +97,43 @@ private ExtImpOperaads parseImpExt(Imp imp) {
try {
return mapper.mapper().convertValue(imp.getExt(), OPERAADS_EXT_TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException(String.format("Missing bidder ext in impression with id: %s", imp.getId()));
throw new PreBidException(e.getMessage());
}
}

private Imp resolveImp(Imp imp, ExtImpOperaads extImpOperaads) {
final Banner banner = imp.getBanner();
final Native xNative = imp.getXNative();
final boolean isNativeResolvable = xNative != null && StringUtils.isNotEmpty(xNative.getRequest());
return imp.toBuilder()
.xNative(isNativeResolvable ? resolveNative(xNative) : xNative)
.banner(banner != null ? resolveBanner(banner) : null)
.tagid(extImpOperaads.getPlacementId())
.build();
private Imp modifyImp(Imp imp, String placementId) {
final Imp.ImpBuilder impBuilder = imp.toBuilder()
.tagid(placementId);

final String impId = imp.getId();
if (imp.getBanner() != null) {
impBuilder.id(buildImpId(impId, BidType.banner))
.banner(modifyBanner(imp.getBanner()))
.video(null)
.xNative(null);
} else if (imp.getVideo() != null) {
impBuilder.id(buildImpId(impId, BidType.video))
.xNative(null);
} else if (imp.getXNative() != null) {
impBuilder.id(buildImpId(impId, BidType.xNative))
.xNative(modifyNative(imp.getXNative()));
} else {
return null;
}

return impBuilder.build();
}

private Native resolveNative(Native xNative) {
try {
final JsonNode nativeNode = mapper.mapper().readTree(xNative.getRequest()).get("native");
if (nativeNode != null && nativeNode.isObject()) {
final JsonNode requestNode = mapper.mapper().createObjectNode().set("native", nativeNode);
return xNative.toBuilder()
.request(mapper.encodeToString(requestNode))
.build();
}
} catch (JsonProcessingException e) {
throw new PreBidException(e.getMessage());
}
return xNative;
private static String buildImpId(String originalId, BidType type) {
return String.format("%s:opa:%s", originalId, type.getName());
}

private static Banner resolveBanner(Banner banner) {
private static Banner modifyBanner(Banner banner) {
final Integer w = banner.getW();
final Integer h = banner.getH();
final List<Format> formats = banner.getFormat();
if (w == null || h == null || w == 0 || h == 0) {

if (w == null || w == 0 || h == null || h == 0) {
if (CollectionUtils.isNotEmpty(formats)) {
final Format firstFormat = formats.get(0);
return banner.toBuilder()
Expand All @@ -134,73 +144,110 @@ private static Banner resolveBanner(Banner banner) {

throw new PreBidException("Size information missing for banner");
}

return banner;
}

private HttpRequest<BidRequest> createRequest(BidRequest bidRequest, Imp imp, ExtImpOperaads extImpOperaads) {
final String resolvedUrl = endpointUrl
.replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(extImpOperaads.getPublisherId()))
.replace(ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(extImpOperaads.getEndpointId()));
private Native modifyNative(Native xNative) {
final JsonNode requestNode;
try {
requestNode = mapper.mapper().readTree(xNative.getRequest());
} catch (JsonProcessingException e) {
throw new PreBidException(e.getMessage());
}

final JsonNode nativeNode = requestNode != null
? requestNode.path("native")
: MissingNode.getInstance();

if (nativeNode.isMissingNode()) {
final JsonNode modifiedRequestNode = mapper.mapper().createObjectNode().set("native", requestNode);
return xNative.toBuilder()
.request(mapper.encodeToString(modifiedRequestNode))
.build();
}

return xNative;
}

private HttpRequest<BidRequest> createRequest(BidRequest bidRequest, Imp imp, ExtImpOperaads extImpOperaads) {
final BidRequest outgoingRequest = bidRequest.toBuilder()
.imp(Collections.singletonList(imp))
.build();

return HttpRequest.<BidRequest>builder()
.method(HttpMethod.POST)
.uri(resolvedUrl)
.uri(resolveUrl(extImpOperaads))
.headers(HttpUtil.headers())
.payload(outgoingRequest)
.body(mapper.encodeToBytes(outgoingRequest))
.build();
}

private String resolveUrl(ExtImpOperaads extImpOperaads) {
return endpointUrl
.replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(extImpOperaads.getPublisherId()))
.replace(ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(extImpOperaads.getEndpointId()));
}

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

private static List<BidderBid> extractBids(BidRequest bidRequest, BidResponse bidResponse) {
private List<BidderBid> extractBids(BidResponse bidResponse) {
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
return Collections.emptyList();
}
return bidsFromResponse(bidRequest, bidResponse);
}

private static List<BidderBid> bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) {
return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(OperaadsBidder::isBidValid)
.map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()),
bidResponse.getCur()))
.filter(OperaadsBidder::isValidBid)
.map(this::createBidderBid)
.collect(Collectors.toList());
}

private static boolean isBidValid(Bid bid) {
final BigDecimal price = bid.getPrice();
return price != null && price.compareTo(BigDecimal.ZERO) > 0;
private static boolean isValidBid(Bid bid) {
return BidderUtil.isValidPrice(ObjectUtil.getIfNotNull(bid, Bid::getPrice));
}

private static BidType getBidType(String impId, List<Imp> imps) {
for (Imp imp : imps) {
if (impId.equals(imp.getId())) {
if (imp.getVideo() != null) {
return BidType.video;
} else if (imp.getXNative() != null) {
return BidType.xNative;
}
return BidType.banner;
}
private BidderBid createBidderBid(Bid bid) {
final String[] idParts = StringUtils.split(bid.getImpid(), ":");

if (idParts == null || idParts.length < 2) {
throw new PreBidException("BidType not provided.");
}

return BidderBid.of(
modifyBid(bid, constructImpId(idParts)),
parseBidType(idParts[idParts.length - 1]),
BIDDER_CURRENCY);
}

private static Bid modifyBid(Bid bid, String impId) {
return bid.toBuilder().impid(impId).build();
}

private static String constructImpId(String[] idParts) {
return Arrays.stream(idParts)
.limit(idParts.length - 2)
.collect(Collectors.joining(":"));
}

private BidType parseBidType(String bidType) {
try {
return mapper.mapper().convertValue(bidType, BidType.class);
} catch (IllegalArgumentException e) {
throw new PreBidException(e.getMessage());
}
return BidType.banner;
}
}

Loading

0 comments on commit ed4509f

Please sign in to comment.