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

Core: Add new AdQuery bidder #2922

Merged
merged 8 commits into from
Feb 7, 2024
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
207 changes: 207 additions & 0 deletions src/main/java/org/prebid/server/bidder/adquery/AdQueryBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package org.prebid.server.bidder.adquery;

import com.fasterxml.jackson.core.type.TypeReference;
import com.iab.openrtb.request.Banner;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Device;
import com.iab.openrtb.request.Format;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.request.Site;
import com.iab.openrtb.request.User;
import com.iab.openrtb.response.Bid;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpMethod;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.bidder.Bidder;
import org.prebid.server.bidder.adquery.model.request.AdQueryRequest;
import org.prebid.server.bidder.adquery.model.response.AdQueryDataResponse;
import org.prebid.server.bidder.adquery.model.response.AdQueryMediaType;
import org.prebid.server.bidder.adquery.model.response.AdQueryResponse;
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.adquery.ExtImpAdQuery;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.HttpUtil;
import org.prebid.server.util.ObjectUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

public class AdQueryBidder implements Bidder<AdQueryRequest> {

private static final TypeReference<ExtPrebid<?, ExtImpAdQuery>> AD_QUERY_EXT_TYPE_REFERENCE =
new TypeReference<>() {
};
private static final String PREBID_VERSION = "server";
private static final String BIDDER_NAME = "adquery";
private static final String DEFAULT_CURRENCY = "PLN";
private static final String ORTB_VERSION = "2.5";
private static final String ADM_TEMPLATE = "<script src=\"%s\"></script>%s";
private static final String FORMAT_TEMPLATE = "%sx%s";

private final String endpointUrl;
private final JacksonMapper mapper;

public AdQueryBidder(String endpointUrl, JacksonMapper mapper) {
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
this.mapper = Objects.requireNonNull(mapper);
}

@Override
public Result<List<HttpRequest<AdQueryRequest>>> makeHttpRequests(BidRequest request) {
final List<BidderError> errors = new ArrayList<>();
final List<HttpRequest<AdQueryRequest>> httpRequests = new ArrayList<>();

for (Imp imp : request.getImp()) {
final ExtImpAdQuery extImpAdQuery;
try {
extImpAdQuery = parseImpExt(imp);
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
continue;
}
httpRequests.add(createRequest(request, imp, extImpAdQuery));
}

return Result.of(httpRequests, errors);
}

private ExtImpAdQuery parseImpExt(Imp imp) {
try {
return mapper.mapper().convertValue(imp.getExt(), AD_QUERY_EXT_TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException(e.getMessage());
}
}

private HttpRequest<AdQueryRequest> createRequest(BidRequest bidRequest, Imp imp, ExtImpAdQuery extImpAdQuery) {
final AdQueryRequest outgoingRequest = createAdQueryRequest(bidRequest, imp, extImpAdQuery);

return HttpRequest.<AdQueryRequest>builder()
.method(HttpMethod.POST)
.uri(endpointUrl)
.headers(makeHeaders(bidRequest.getDevice()))
.impIds(Collections.singleton(imp.getId()))
.payload(outgoingRequest)
.body(mapper.encodeToBytes(outgoingRequest))
.build();
}

private static AdQueryRequest createAdQueryRequest(BidRequest bidRequest, Imp imp, ExtImpAdQuery extImpAdQuery) {
final Optional<Device> optionalDevice = Optional.ofNullable(bidRequest.getDevice());
return AdQueryRequest.builder()
.version(PREBID_VERSION)
.placementCode(extImpAdQuery.getPlacementId())
.auctionId(StringUtils.EMPTY)
.type(extImpAdQuery.getType())
.adUnitCode(imp.getTagid())
.bidQid(Optional.ofNullable(bidRequest.getUser()).map(User::getId).orElse(StringUtils.EMPTY))
.bidId(StringUtils.defaultString(bidRequest.getId()) + StringUtils.defaultString(imp.getId()))
.bidder(BIDDER_NAME)
.bidderRequestId(bidRequest.getId())
.bidRequestsCount(1)
.bidderRequestsCount(1)
.sizes(getImpSizes(imp))
.bidIp(optionalDevice.map(Device::getIp).orElse(null))
.bidIpv6(optionalDevice.map(Device::getIpv6).orElse(null))
.bidUa(optionalDevice.map(Device::getUa).orElse(null))
.bidPageUrl(Optional.ofNullable(bidRequest.getSite()).map(Site::getPage).orElse(null))
.build();
}

private static String getImpSizes(Imp imp) {
final Banner banner = imp.getBanner();
if (banner == null) {
return StringUtils.EMPTY;
}

final List<Format> format = banner.getFormat();
if (CollectionUtils.isNotEmpty(format)) {
return format.stream()
.map(singleFormat -> FORMAT_TEMPLATE.formatted(
ObjectUtils.defaultIfNull(singleFormat.getW(), 0),
ObjectUtils.defaultIfNull(singleFormat.getH(), 0)))
.collect(Collectors.joining(","));
}
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved

final Integer w = banner.getW();
final Integer h = banner.getH();
if (w != null && h != null) {
return FORMAT_TEMPLATE.formatted(w, h);
}

return StringUtils.EMPTY;
}

private static MultiMap makeHeaders(Device device) {
final MultiMap headers = HttpUtil.headers();
headers.add(HttpUtil.X_OPENRTB_VERSION_HEADER, ORTB_VERSION);

Optional.ofNullable(device)
.map(Device::getIp)
.filter(StringUtils::isNotBlank)
.ifPresent(ip -> headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip));

return headers;
}

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

private static List<BidderBid> extractBids(AdQueryResponse adQueryResponse, String bidRequestId) {
if (adQueryResponse == null || adQueryResponse.getData() == null) {
return Collections.emptyList();
}

final AdQueryDataResponse data = adQueryResponse.getData();
final AdQueryMediaType mediaType = data.getAdQueryMediaType();
final Bid bid = Bid.builder()
.id(data.getRequestId())
.impid(resolveImpId(bidRequestId, data.getRequestId()))
.price(data.getCpm())
.adm(ADM_TEMPLATE.formatted(data.getAdqLib(), data.getTag()))
.adomain(data.getAdDomains())
.crid(data.getCreationId())
.w(ObjectUtil.getIfNotNull(mediaType, AdQueryMediaType::getWidth))
.h(ObjectUtil.getIfNotNull(mediaType, AdQueryMediaType::getHeight))
.build();

final BidType bidType = ObjectUtil.getIfNotNull(mediaType, AdQueryMediaType::getName);
final String currency = StringUtils.isNotBlank(data.getCurrency()) ? data.getCurrency() : DEFAULT_CURRENCY;
return Collections.singletonList(BidderBid.of(bid, resolveMediaType(bidType), currency));
}

private static String resolveImpId(String bidRequestId, String adQueryRequestId) {
return adQueryRequestId != null
? bidRequestId.replaceAll(adQueryRequestId, StringUtils.EMPTY)
: bidRequestId;
}

private static BidType resolveMediaType(BidType bidType) {
if (bidType != BidType.banner) {
throw new PreBidException("Unsupported MediaType: %s".formatted(bidType));
}
return BidType.banner;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.prebid.server.bidder.adquery.model.request;

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

@Builder(toBuilder = true)
@Value
public class AdQueryRequest {
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved

@JsonProperty("v")
String version;

@JsonProperty("placementCode")
String placementCode;

@JsonProperty("auctionId")
String auctionId;

String type;

@JsonProperty("adUnitCode")
String adUnitCode;

@JsonProperty("bidQid")
String bidQid;

@JsonProperty("bidId")
String bidId;

@JsonProperty("bidIp")
String bidIp;

@JsonProperty("bidIpv6")
String bidIpv6;

@JsonProperty("bidUa")
String bidUa;

String bidder;
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved

@JsonProperty("bidPageUrl")
String bidPageUrl;

@JsonProperty("bidderRequestId")
String bidderRequestId;

@JsonProperty("bidRequestsCount")
Integer bidRequestsCount;

@JsonProperty("bidderRequestsCount")
Integer bidderRequestsCount;

String sizes;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.prebid.server.bidder.adquery.model.response;

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

import java.math.BigDecimal;
import java.util.List;

@Builder(toBuilder = true)
@Value(staticConstructor = "of")
public class AdQueryDataResponse {

@JsonProperty("requestId")
String requestId;

@JsonProperty("creationId")
String creationId;

String currency;

BigDecimal cpm;

String code;

@JsonProperty("adqLib")
String adqLib;

String tag;

@JsonProperty("adDomains")
List<String> adDomains;

@JsonProperty("dealid")
String dealId;

@JsonProperty("mediaType")
AdQueryMediaType adQueryMediaType;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.prebid.server.bidder.adquery.model.response;

import lombok.Value;
import org.prebid.server.proto.openrtb.ext.response.BidType;

@Value(staticConstructor = "of")
public class AdQueryMediaType {

BidType name;

Integer width;

Integer height;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.prebid.server.bidder.adquery.model.response;

import lombok.Value;

@Value(staticConstructor = "of")
public class AdQueryResponse {

AdQueryDataResponse data;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.prebid.server.proto.openrtb.ext.request.adquery;

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

@Value(staticConstructor = "of")
public class ExtImpAdQuery {

@JsonProperty("placementId")
String placementId;

String type;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.prebid.server.spring.config.bidder;

import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.adquery.AdQueryBidder;
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 javax.validation.constraints.NotBlank;

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

private static final String BIDDER_NAME = "adquery";

@Bean("adqueryConfigurationProperties")
@ConfigurationProperties("adapters.adquery")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

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

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(adqueryConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new AdQueryBidder(config.getEndpoint(), mapper))
.assemble();
}
}
Loading
Loading