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

Kobler: New adapter (#3667) #3684

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
202 changes: 202 additions & 0 deletions src/main/java/org/prebid/server/bidder/kobler/KoblerBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package org.prebid.server.bidder.kobler;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.vertx.core.http.HttpMethod;
import org.apache.commons.collections4.CollectionUtils;
import org.prebid.server.bidder.Bidder;
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.currency.CurrencyConversionService;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.json.DecodeException;
import org.prebid.server.json.EncodeException;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
import org.prebid.server.proto.openrtb.ext.request.kobler.ExtImpKobler;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
import org.prebid.server.util.HttpUtil;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public class KoblerBidder implements Bidder<BidRequest> {

private static final TypeReference<ExtPrebid<?, ExtImpKobler>> KOBLER_EXT_TYPE_REFERENCE =
new TypeReference<>() {
};
private static final String EXT_PREBID = "prebid";
private static final String DEFAULT_BID_CURRENCY = "USD";
private static final String DEV_ENDPOINT = "https://bid-service.dev.essrtb.com/bid/prebid_server_rtb_call";

private final String endpointUrl;
private final CurrencyConversionService currencyConversionService;
private final JacksonMapper mapper;

public KoblerBidder(String endpointUrl,
CurrencyConversionService currencyConversionService,
JacksonMapper mapper) {

this.endpointUrl = HttpUtil.validateUrl(endpointUrl);
this.currencyConversionService = Objects.requireNonNull(currencyConversionService);
this.mapper = Objects.requireNonNull(mapper);
}

@Override
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest bidRequest) {
final List<BidderError> errors = new ArrayList<>();
boolean testMode = false;
final List<Imp> modifiedImps = new ArrayList<>();

final List<String> currencies = bidRequest.getCur() != null
przemkaczmarek marked this conversation as resolved.
Show resolved Hide resolved
? new ArrayList<>(bidRequest.getCur())
: new ArrayList<>();
if (!currencies.contains(DEFAULT_BID_CURRENCY)) {
currencies.add(DEFAULT_BID_CURRENCY);
}

BidRequest modifiedRequest = bidRequest.toBuilder().cur(currencies).build();

for (Imp imp : modifiedRequest.getImp()) {
try {
final Imp processedImp = processImp(modifiedRequest, imp, errors);
modifiedImps.add(processedImp);

if (modifiedImps.size() == 1) {
testMode = extractTestMode(processedImp);
}
przemkaczmarek marked this conversation as resolved.
Show resolved Hide resolved
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}

if (modifiedImps.isEmpty()) {
errors.add(BidderError.badInput("No valid impressions"));
return Result.withErrors(errors);
}

modifiedRequest = modifiedRequest.toBuilder().imp(modifiedImps).build();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please modify the request once


final String endpoint = testMode ? DEV_ENDPOINT : endpointUrl;

try {
return Result.of(Collections.singletonList(
HttpRequest.<BidRequest>builder()
.method(HttpMethod.POST)
.uri(endpoint)
.headers(HttpUtil.headers())
.body(mapper.encodeToBytes(modifiedRequest))
.payload(modifiedRequest)
.build()
), errors);
} catch (EncodeException e) {
errors.add(BidderError.badInput("Failed to encode request: " + e.getMessage()));
return Result.withErrors(errors);
}
przemkaczmarek marked this conversation as resolved.
Show resolved Hide resolved
}

private Imp processImp(BidRequest bidRequest, Imp imp, List<BidderError> errors) {
if (imp.getBidfloor() != null
&& imp.getBidfloor().compareTo(BigDecimal.ZERO) > 0
&& imp.getBidfloorcur() != null) {
final String bidFloorCur = imp.getBidfloorcur().toUpperCase();
if (!DEFAULT_BID_CURRENCY.equals(bidFloorCur)) {
try {
final BigDecimal convertedPrice = currencyConversionService.convertCurrency(
imp.getBidfloor(),
bidRequest,
bidFloorCur,
DEFAULT_BID_CURRENCY
);
return imp.toBuilder()
.bidfloor(convertedPrice)
.bidfloorcur(DEFAULT_BID_CURRENCY)
.build();
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}
}
return imp;
}
przemkaczmarek marked this conversation as resolved.
Show resolved Hide resolved

public boolean extractTestMode(Imp imp) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why public?

try {
final ExtPrebid<?, ExtImpKobler> extPrebid = mapper.mapper().convertValue(imp.getExt(),
KOBLER_EXT_TYPE_REFERENCE);
final ExtImpKobler extImpKobler = extPrebid != null ? extPrebid.getBidder() : null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse the imp.ext in the same way you did before, please make it in the same style other bidders do

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the code. My question is:
-Can we receive an empty ext in the request? If so, the parseImpExt method will return null. shuould I handle NPE? impExtKobler field is not rerequired.

return extImpKobler != null && Boolean.TRUE.equals(extImpKobler.getTest());
} catch (IllegalArgumentException e) {
return false;
}
}

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
try {
final List<BidderError> errors = new ArrayList<>();
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
return Result.of(extractBids(bidResponse, errors), errors);
} catch (DecodeException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

private List<BidderBid> extractBids(BidResponse bidResponse, List<BidderError> errors) {
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
return Collections.emptyList();
}
return bidsFromResponse(bidResponse, errors);
}

private List<BidderBid> bidsFromResponse(BidResponse bidResponse, List<BidderError> errors) {
przemkaczmarek marked this conversation as resolved.
Show resolved Hide resolved
return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur()))
.filter(Objects::nonNull)
.toList();
}

private BidType getBidType(Bid bid) {
if (bid.getExt() == null) {
return BidType.banner;
}

final ObjectNode prebidNode = (ObjectNode) bid.getExt().get(EXT_PREBID);
if (prebidNode == null) {
return BidType.banner;
}

final ExtBidPrebid extBidPrebid = parseExtBidPrebid(prebidNode);
if (extBidPrebid == null || extBidPrebid.getType() == null) {
return BidType.banner;
}

return extBidPrebid.getType(); // jeśli udało się sparsować
}

private ExtBidPrebid parseExtBidPrebid(ObjectNode prebid) {
try {
return mapper.mapper().treeToValue(prebid, ExtBidPrebid.class);
} catch (JsonProcessingException e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.prebid.server.proto.openrtb.ext.request.kobler;

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

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

@JsonProperty("test")
Boolean test;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.prebid.server.spring.config.bidder;

import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.kobler.KoblerBidder;
import org.prebid.server.currency.CurrencyConversionService;
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 jakarta.validation.constraints.NotBlank;

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

private static final String BIDDER_NAME = "kobler";

@Bean("koblerConfigurationProperties")
@ConfigurationProperties("adapters.kobler")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

@Bean
BidderDeps koblerBidderDeps(BidderConfigurationProperties koblerConfigurationProperties,
CurrencyConversionService currencyConversionService,
@NotBlank @Value("${external-url}") String externalUrl,
JacksonMapper mapper) {
przemkaczmarek marked this conversation as resolved.
Show resolved Hide resolved

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(koblerConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new KoblerBidder(config.getEndpoint(), currencyConversionService, mapper))
.assemble();
}
}
13 changes: 13 additions & 0 deletions src/main/resources/bidder-config/kobler.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
adapters:
kobler:
endpoint: "https://bid.essrtb.com/bid/prebid_server_rtb_call"
przemkaczmarek marked this conversation as resolved.
Show resolved Hide resolved
endpoint-compression: gzip
geoscope:
- NOR
- SWE
- DNK
meta-info:
maintainer-email: [email protected]
site-media-types:
- banner
vendor-id: 0
13 changes: 13 additions & 0 deletions src/main/resources/static/bidder-params/kobler.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Kobler Adapter Params",
"description": "A schema which validates params accepted by the Kobler adapter",
"type": "object",

"properties": {
"test": {
"type": "boolean",
"description": "Whether the request is for testing only. When multiple ad units are submitted together, it is enough to set this parameter on the first one."
}
}
}
Loading
Loading