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 3 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
205 changes: 205 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,205 @@
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.BidderUtil;
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 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(resolveHeader(bidRequest.getDevice()))
.impIds(BidderUtil.impIds(bidRequest))
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
.payload(outgoingRequest)
.body(mapper.encodeToBytes(outgoingRequest))
.build();
}

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 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 -> "%sx%s".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 "%sx%s".formatted(w, h);
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
}

return StringUtils.EMPTY;
}

private MultiMap resolveHeader(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)
.map(StringUtils::isNotBlank)
.ifPresent(ip -> headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()));
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved

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));
} catch (DecodeException | PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

private static List<BidderBid> extractBids(AdQueryResponse adQueryResponse, BidRequest bidRequest) {
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
if (adQueryResponse == null || adQueryResponse.getData() == null) {
return Collections.emptyList();
}

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

return Collections.singletonList(BidderBid.of(bid, resolveMediaType(data.getAdQueryMediaType()),
StringUtils.isNotBlank(data.getCurrency()) ? data.getCurrency() : DEFAULT_CURRENCY));
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
}

private static String resolveImpId(BidRequest bidRequest, AdQueryDataResponse data) {
return data.getRequestId() != null
? bidRequest.getId().replaceAll(data.getRequestId(), StringUtils.EMPTY)
: bidRequest.getId();
}

private static BidType resolveMediaType(AdQueryMediaType mediaType) {
if (mediaType.getName() != BidType.banner) {
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
throw new PreBidException(String.format("Unsupported MediaType: %s", mediaType.getName()));
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
}
return BidType.banner;
}
}
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();
}
}
16 changes: 16 additions & 0 deletions src/main/resources/bidder-config/adquery.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
adapters:
adquery:
endpoint: https://bidder2.adquery.io/prebid/bid
meta-info:
maintainer-email: prebid@adquery.io
app-media-types:
site-media-types:
- banner
supported-vendors:
vendor-id: 902
usersync:
cookie-family-name: adquery
iframe:
url: https://api.adquery.io/storage?gdpr={{gdpr}}&consent={{gdpr_consent}}&ccpa_consent={{us_privacy}}&redirect={{redirect_url}}
support-cors: false
uid-macro: '$UID'
Loading
Loading