Skip to content
Merged
218 changes: 218 additions & 0 deletions src/main/java/org/prebid/server/bidder/connatix/ConnatixBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package org.prebid.server.bidder.connatix;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.App;
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.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.vertx.core.MultiMap;
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.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.ExtApp;
import org.prebid.server.proto.openrtb.ext.request.connatix.ExtImpConnatix;
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;
import java.util.Optional;

public class ConnatixBidder implements Bidder<BidRequest> {

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

private static final String BIDDER_CURRENCY = "USD";
private static final String FORMATTING = "%s-%s";

private final String endpointUrl;
private final JacksonMapper mapper;

private final CurrencyConversionService currencyConversionService;

public ConnatixBidder(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) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

please fix a method order according to the code style

final Device device = request.getDevice();

if (device == null
|| (device.getIp() == null && device.getIpv6() == null)) {
return Result.withError(BidderError.badInput("Device IP is required"));
}

final String displayManagerVer = buildDisplayManagerVersion(request);
final MultiMap headers = resolveHeaders(device);

final List<HttpRequest<BidRequest>> httpRequests = new ArrayList<>();
final List<BidderError> errors = new ArrayList<>();

for (Imp imp : request.getImp()) {
try {
final ExtImpConnatix extImpConnatix = parseExtImp(imp);
final Imp modifiedImp = modifyImp(imp, extImpConnatix, displayManagerVer, request);

httpRequests.add(makeHttpRequest(request, modifiedImp, headers));
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}

return Result.of(httpRequests, errors);
}

private static String buildDisplayManagerVersion(BidRequest request) {
return Optional.ofNullable(request.getApp())
.map(App::getExt)
.map(ExtApp::getPrebid)
.filter(prebid -> ObjectUtils.allNotNull(prebid.getSource(), prebid.getVersion()))
.map(prebid -> FORMATTING.formatted(prebid.getSource(), prebid.getVersion()))
.orElse(StringUtils.EMPTY);
}

private static MultiMap resolveHeaders(Device device) {
final MultiMap headers = HttpUtil.headers();
if (device != null) {
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa());
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6());
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp());
}
return headers;
}

private ExtImpConnatix parseExtImp(Imp imp) {
try {
return mapper.mapper().convertValue(imp.getExt(), CONNATIX_EXT_TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException(e.getMessage());
}
}

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

final ObjectNode impExt = mapper.mapper()
.createObjectNode().set("connatix", mapper.mapper().valueToTree(extImpConnatix));

return imp.toBuilder()
.ext(impExt)
.banner(modifyImpBanner(imp.getBanner()))
.displaymanagerver(StringUtils.isBlank(imp.getDisplaymanagerver())
&& StringUtils.isNotBlank(displayManagerVer)
? displayManagerVer
: imp.getDisplaymanagerver())
.bidfloor(bidFloorPrice.getValue())
.bidfloorcur(bidFloorPrice.getCurrency())
.build();
}

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

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

return Price.of(BIDDER_CURRENCY, convertedPrice);
}

private Banner modifyImpBanner(Banner banner) {
if (banner == null) {
return null;
}

if (banner.getW() == null && banner.getH() == null && CollectionUtils.isNotEmpty(banner.getFormat())) {
final Format firstFormat = banner.getFormat().getFirst();
return banner.toBuilder()
.w(firstFormat.getW())
.h(firstFormat.getH())
.build();
}
return banner;
}

private HttpRequest<BidRequest> makeHttpRequest(BidRequest request, Imp imp, MultiMap headers) {
final BidRequest outgoingRequest = request.toBuilder()
.imp(List.of(imp))
.build();

return BidderUtil.defaultRequest(outgoingRequest, headers, endpointUrl, mapper);
}

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
final List<BidderBid> bids = extractBids(bidResponse);

return Result.withValues(bids);
} catch (DecodeException | PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

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

return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.map(bid -> BidderBid.of(bid, getBidType(bid), BIDDER_CURRENCY))
.toList();
}

private static BidType getBidType(Bid bid) {
return Optional.ofNullable(bid.getExt())
.map(ext -> ext.get("connatix"))
.map(cnx -> cnx.get("mediaType"))
.map(JsonNode::asText)
.filter(type -> Objects.equals(type, "video"))
.map(ignored -> BidType.video)
.orElse(BidType.banner);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.prebid.server.proto.openrtb.ext.request.connatix;

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

import java.math.BigDecimal;

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

@JsonProperty("placementId")
String placementId;

@JsonProperty("viewabilityPercentage")
BigDecimal viewabilityPercentage;

}
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.connatix.ConnatixBidder;
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/connatix.yaml", factory = YamlPropertySourceFactory.class)
public class ConnatixConfiguration {

private static final String BIDDER_NAME = "connatix";

@Bean("connatixConfigurationProperties")
@ConfigurationProperties("adapters.connatix")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

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

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(connatixConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new ConnatixBidder(config.getEndpoint(), currencyConversionService, mapper))
.assemble();
}
}
23 changes: 23 additions & 0 deletions src/main/resources/bidder-config/connatix.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
adapters:
connatix:
endpoint: "https://capi.connatix.com/rtb/ortb"
meta-info:
maintainer-email: "pubsolutions@connatix.com"
vendor-id: 143
app-media-types:
- banner
- video
site-media-types:
- banner
- video
usersync:
cookie-family-name: connatix
iframe:
url: "https://capi.connatix.com/us/pixel?pId=53&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&callback={{redirect_url}}"
uid-macro: '[UID]'
support-cors: false
redirect:
url: "https://capi.connatix.com/us/pixel?pId=52&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&callback={{redirect_url}}"
uid-macro: '[UID]'
support-cors: false

21 changes: 21 additions & 0 deletions src/main/resources/static/bidder-params/connatix.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Connatix Adapter Params",
"description": "A schema which validates params accepted by the Connatix adapter",
"type": "object",
"properties": {
"placementId": {
"type": "string",
"minLength": 1,
"description": "Placement ID"
},
"viewabilityPercentage": {
"type": "number",
"description": "Declared viewability percentage (values from 0 to 1, where 1 = 100%)",
"minimum": 0,
"maximum": 1
}
},
"required": ["placementId"]
}

Loading
Loading