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 5 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());
}
}

@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 AdQueryRequest createAdQueryRequest(BidRequest bidRequest, Imp imp, ExtImpAdQuery extImpAdQuery) {
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
final Optional<Device> optionalDevice = Optional.ofNullable(bidRequest.getDevice());
return AdQueryRequest.builder()
.v(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(bidRequest.getId() + imp.getId())
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
.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 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;
}

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 String getImpSizes(Imp imp) {
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
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
}
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 MultiMap makeHeaders(Device device) {
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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

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

@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