Skip to content

Commit

Permalink
LimeLightDigital: Add adapter (#2228)
Browse files Browse the repository at this point in the history
  • Loading branch information
SerhiiNahornyi authored Mar 15, 2023
1 parent ce66cfd commit 34f424b
Show file tree
Hide file tree
Showing 18 changed files with 1,046 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package org.prebid.server.bidder.limelightdigital;

import com.fasterxml.jackson.core.type.TypeReference;
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.apache.commons.lang3.StringUtils;
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.Price;
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.JacksonMapper;
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
import org.prebid.server.proto.openrtb.ext.request.limelightdigital.ExtImpLimeLightDigital;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.BidderUtil;
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 LimeLightDigitalBidder implements Bidder<BidRequest> {

private static final TypeReference<ExtPrebid<?, ExtImpLimeLightDigital>> LIME_LIGHT_EXT_TYPE_REFERENCE =
new TypeReference<>() {
};

private static final String BIDDER_CURRENCY = "USD";
private static final String URL_HOST_MACRO = "{{Host}}";
private static final String URL_PUBLISHER_ID_MACRO = "{{PublisherID}}";

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

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

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

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

for (Imp imp : request.getImp()) {
try {
final ExtImpLimeLightDigital extImpAdview = parseExtImp(imp);
final String endpointUri = resolveEndpoint(extImpAdview);
final Imp modifiedImp = modifyImp(imp, request);
final BidRequest modifiedBidRequest = modifyRequest(request, modifiedImp);

requests.add(createHttpRequest(modifiedBidRequest, endpointUri));
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}

return Result.of(requests, errors);
}

private ExtImpLimeLightDigital parseExtImp(Imp imp) {
try {
return mapper.mapper().convertValue(imp.getExt(), LIME_LIGHT_EXT_TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException("ext.bidder is not provided");
}
}

private String resolveEndpoint(ExtImpLimeLightDigital extImp) {
final String host = extImp.getHost();
final int firstDotIndex = StringUtils.indexOf(host, ".");
if (firstDotIndex < 1 || firstDotIndex == StringUtils.length(host) - 1) {
throw new PreBidException("Hostname is invalid: " + host);
}

final String publisherId = String.valueOf(extImp.getPublisherId());
return endpointUrl.replace(URL_HOST_MACRO, HttpUtil.encodeUrl(host))
.replace(URL_PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(publisherId));
}

private Imp modifyImp(Imp imp, BidRequest request) {
final Price bidFloorPrice = resolveBidFloor(imp, request);

return imp.toBuilder()
.bidfloor(bidFloorPrice.getValue())
.bidfloorcur(bidFloorPrice.getCurrency())
.ext(null)
.build();
}

private Price resolveBidFloor(Imp imp, BidRequest bidRequest) {
final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor());
return BidderUtil.isValidPrice(initialBidFloorPrice)
? convertBidFloor(initialBidFloorPrice, imp.getId(), bidRequest)
: initialBidFloorPrice;
}

private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidRequest) {
final String bidFloorCur = bidFloorPrice.getCurrency();
try {
final BigDecimal convertedPrice = currencyConversionService
.convertCurrency(bidFloorPrice.getValue(), bidRequest, bidFloorCur, BIDDER_CURRENCY);

return Price.of(BIDDER_CURRENCY, convertedPrice);
} catch (PreBidException e) {
throw new PreBidException("Unable to convert provided bid floor currency from %s to %s for imp `%s`"
.formatted(bidFloorCur, BIDDER_CURRENCY, impId));
}
}

private static BidRequest modifyRequest(BidRequest bidRequest, Imp imp) {
return bidRequest.toBuilder()
.id(bidRequest.getId() + "-" + imp.getId())
.imp(Collections.singletonList(imp))
.ext(null)
.build();
}

private HttpRequest<BidRequest> createHttpRequest(BidRequest modifiedBidRequest, String endpointUri) {
return HttpRequest.<BidRequest>builder()
.method(HttpMethod.POST)
.uri(endpointUri)
.headers(HttpUtil.headers())
.body(mapper.encodeToBytes(modifiedBidRequest))
.payload(modifiedBidRequest)
.build();
}

@Override
public final 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(httpCall.getRequest().getPayload(), bidResponse, errors), errors);
} catch (DecodeException | PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

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

private static List<BidderBid> bidsFromResponse(BidRequest bidRequest,
BidResponse bidResponse,
List<BidderError> errors) {
return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.map(bid -> resolveBidderBid(bid, bidResponse.getCur(), bidRequest.getImp(), errors))
.filter(Objects::nonNull)
.toList();
}

private static BidderBid resolveBidderBid(Bid bid, String currency, List<Imp> imps, List<BidderError> errors) {
final BidType bidType;
try {
bidType = getBidType(bid.getImpid(), imps);
} catch (PreBidException e) {
errors.add(BidderError.badServerResponse(e.getMessage()));
return null;
}
return BidderBid.of(bid, bidType, currency);
}

private static BidType getBidType(String impId, List<Imp> imps) {
for (Imp imp : imps) {
if (imp.getId().equals(impId)) {
if (imp.getBanner() != null) {
return BidType.banner;
}
if (imp.getVideo() != null) {
return BidType.video;
}
if (imp.getAudio() != null) {
return BidType.audio;
}
if (imp.getXNative() != null) {
return BidType.xNative;
}
throw new PreBidException("Unknown media type of imp: '%s'".formatted(impId));
}
}
throw new PreBidException("Bid contains unknown imp id: '%s'".formatted(impId));
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package org.prebid.server.proto.openrtb.ext.request.between;

import lombok.AllArgsConstructor;
import lombok.Value;

@AllArgsConstructor(staticName = "of")
@Value
@Value(staticConstructor = "of")
public class ExtImpBetween {

String host;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.prebid.server.proto.openrtb.ext.request.limelightdigital;

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

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

String host;

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

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

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

private static final String BIDDER_NAME = "limelightDigital";

@Bean("limelightDigitalConfigurationProperties")
@ConfigurationProperties("adapters.limelightdigital")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

@Bean
BidderDeps limeLightDigitalBidderDeps(BidderConfigurationProperties limelightDigitalConfigurationProperties,
@NotBlank @Value("${external-url}") String externalUrl,
CurrencyConversionService currencyConversionService,
JacksonMapper mapper) {

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(limelightDigitalConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config ->
new LimeLightDigitalBidder(config.getEndpoint(), currencyConversionService, mapper))
.assemble();
}
}
21 changes: 21 additions & 0 deletions src/main/resources/bidder-config/limelightDigital.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
adapters:
limelightDigital:
endpoint: http://{{Host}}.ortb.net/openrtb/{{PublisherID}}
aliases:
iionads:
enabled: false
endpoint: http://{{Host}}.iionads.com/openrtb/{{PublisherID}}
meta-info:
maintainer-email: [email protected]
app-media-types:
- banner
- video
- audio
- native
site-media-types:
- banner
- video
- audio
- native
supported-vendors:
vendor-id: 0
27 changes: 27 additions & 0 deletions src/main/resources/static/bidder-params/limelightDigital.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Limelight Digital Adapter Params",
"description": "A schema which validates params accepted by the Limelight Digital adapter",
"type": "object",
"properties": {
"host": {
"type": "string",
"description": "Ad network's RTB host",
"format": "hostname",
"pattern": "^.+\\..+$"
},
"publisherId": {
"type": [
"integer",
"string"
],
"description": "Publisher ID",
"minimum": 1,
"pattern": "^[1-9][0-9]*$"
}
},
"required": [
"host",
"publisherId"
]
}
Loading

0 comments on commit 34f424b

Please sign in to comment.