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 1 commit
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
213 changes: 213 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,213 @@
package org.prebid.server.bidder.adquery;

import com.fasterxml.jackson.core.io.BigDecimalParser;
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.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;

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
return AdQueryRequest.builder()
.v(PREBID_VERSION)
.placementCode(extImpAdQuery.getPlacementId())
.auctionId(StringUtils.EMPTY)
.type(extImpAdQuery.getType())
.adUnitCode(imp.getTagid())
.bidQid(ObjectUtil.getIfNotNullOrDefault(bidRequest.getUser(), User::getId, () -> StringUtils.EMPTY))
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
.bidId(String.format("%s%s", 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(ObjectUtil.getIfNotNull(bidRequest.getDevice(), Device::getIp))
.bidIpv6(ObjectUtil.getIfNotNull(bidRequest.getDevice(), Device::getIpv6))
.bidUa(ObjectUtil.getIfNotNull(bidRequest.getDevice(), Device::getUa))
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
.bidPageUrl(ObjectUtil.getIfNotNull(bidRequest.getSite(), Site::getPage))
.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)) {
final List<String> sizes = new ArrayList<>();
format.forEach(singleFormat -> sizes.add(
"%sx%s".formatted(getIntOrElseZero(singleFormat.getW()), getIntOrElseZero(singleFormat.getH()))));
return String.join("_", sizes);
}
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 int getIntOrElseZero(Integer number) {
return number != null ? number : 0;
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
}

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);

if (Objects.nonNull(device) && StringUtils.isNotBlank(device.getIp()) && device.getIp().length() > 0) {
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp());
}

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 || Objects.isNull(adQueryResponse.getData())) {
return Collections.emptyList();
}

final AdQueryDataResponse data = adQueryResponse.getData();
final Bid bid = Bid.builder()
.id(data.getRequestId())
.impid(resoleImpId(bidRequest, data))
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
.price(BigDecimalParser.parse(data.getCpm()))
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
.adm(String.format(ADM_TEMPLATE, data.getAdqLib(), data.getTag()))
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
.adomain(data.getAdDomains())
.crid(String.format("%d", data.getCreationId()))
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
.w(parseMeasure(ObjectUtil.getIfNotNull(data.getAdQueryMediaType(), AdQueryMediaType::getWidth)))
.h(parseMeasure(ObjectUtil.getIfNotNull(data.getAdQueryMediaType(), AdQueryMediaType::getHeight)))
.build();

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

private static String resoleImpId(BidRequest bidRequest, AdQueryDataResponse data) {
return Objects.nonNull(data.getRequestId())
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
? bidRequest.getId().replaceAll(data.getRequestId(), StringUtils.EMPTY)
: bidRequest.getId();
}

private static Integer parseMeasure(String measure) {
try {
return Integer.valueOf(measure);
} catch (NumberFormatException e) {
throw new PreBidException("Value of measure: %s can not be parsed.".formatted(measure));
}
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
}

private static BidType resolveMediaType(AdQueryDataResponse data) {
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
if (data.getAdQueryMediaType().getName() != BidType.banner) {
throw new PreBidException(String.format("Unsupported MediaType: %s", data.getAdQueryMediaType().getName()));
}
return data.getAdQueryMediaType().getName();
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.prebid.server.bidder.adquery.model.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;

@Builder(toBuilder = true)
@EqualsAndHashCode
@Getter
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")
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
String adUnitCode;

@JsonProperty("bidQid")
String bidQid;

@JsonProperty("bidId")
String bidId;

@JsonProperty("bidIp")
String bidIp;

@JsonProperty("bidIpv6")
String bidIpv6;

@JsonProperty("bidUa")
String bidUa;

@JsonProperty("bidder")
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,37 @@
package org.prebid.server.bidder.adquery.model.response;

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

import java.util.List;

@Builder(toBuilder = true)
@Getter
public class AdQueryDataResponse {

@JsonProperty("requestId")
String requestId;

@JsonProperty("creationId")
Integer creationId;

String currency;

String cpm;

String code;

@JsonProperty("adqLib")
String adqLib;

String tag;

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

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

@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;

String width;

String 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