Skip to content
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

Operaads: support multiformat request #1709

Merged
merged 4 commits into from
Feb 21, 2022
Merged
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
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