Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions sample/prebid-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ adapters:
enabled: true
rubicon:
enabled: true
tpmn:
enabled: true
metrics:
prefix: prebid
cache:
Expand Down
226 changes: 226 additions & 0 deletions src/main/java/org/prebid/server/bidder/tpmn/TpmnBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package org.prebid.server.bidder.tpmn;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.iab.openrtb.request.*;
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.bidder.Bidder;
import org.prebid.server.bidder.model.*;
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.tpmn.ExtImpTpmn;
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.math.BigDecimal;
import java.util.*;

public class TpmnBidder implements Bidder<BidRequest> {

private static final TypeReference<ExtPrebid<?, ExtImpTpmn>> TPMN_EXT_TYPE_REFERENCE = new TypeReference<>() {};
private static final String BIDDER_CURRENCY = "USD";

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

public TpmnBidder(String endpointUrl, JacksonMapper mapper, CurrencyConversionService currencyConversionService) {
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
this.mapper = Objects.requireNonNull(mapper);
this.currencyConversionService = Objects.requireNonNull(currencyConversionService);
}

@Override
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
final List<Imp> validImps = new ArrayList<>();
final List<BidderError> errors = new ArrayList<>();
for (Imp imp : request.getImp()) {
try {
final ExtImpTpmn extImpTpmn = parseImpExt(imp);
final Imp updatedImp = modifyImp(imp, extImpTpmn, request);
if (updatedImp != null) validImps.add(updatedImp);
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
continue;
}
}

final BidRequest outgoingRequest = request.toBuilder().imp(validImps).build();
return Result.of(Collections.singletonList(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)), errors);

}

private ExtImpTpmn parseImpExt(Imp imp) {
try {
return mapper.mapper().convertValue(imp.getExt(), TPMN_EXT_TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException(e.getMessage());
}
}

private Imp modifyImp(Imp imp, ExtImpTpmn extImpTpmn, BidRequest request) {
Integer inventoryId = extImpTpmn.getInventoryId();
final Imp.ImpBuilder impBuilder = imp.toBuilder().tagid(String.valueOf(inventoryId));
final String impId = imp.getId();
final Price resolvedBidFloor = resolveBidFloor(imp, request);

if (imp.getBanner() != null) {
impBuilder.id(impId).banner(modifyBanner(imp.getBanner())).video(null).xNative(null);
} else if (imp.getVideo() != null) {
impBuilder.id(impId).xNative(null);
} else if (imp.getXNative() != null) {
impBuilder.id(impId).xNative(modifyNative(imp.getXNative()));
} else {
return null;
}

return impBuilder
.bidfloor(resolvedBidFloor.getValue())
.bidfloorcur(resolvedBidFloor.getCurrency())
.ext(mapper.mapper().valueToTree(extImpTpmn))
.build();
}




private static Banner modifyBanner(Banner banner) {
final Integer w = banner.getW();
final Integer h = banner.getH();
final List<Format> formats = banner.getFormat();

if (w == null || w == 0 || h == null || h == 0) {
if (CollectionUtils.isNotEmpty(formats)) {
final Format firstFormat = formats.get(0);
return banner.toBuilder()
.w(firstFormat.getW())
.h(firstFormat.getH())
.build();
}

throw new PreBidException("Size information missing for banner");
}

return banner;
}

private Native modifyNative(Native xNative) {
final JsonNode requestNode;
try {
requestNode = mapper.mapper().readTree(xNative.getRequest());
} catch (JsonProcessingException e) {
throw new PreBidException(e.getMessage());
}

final JsonNode nativeNode = requestNode != null
? requestNode.path("native")
: MissingNode.getInstance();

if (nativeNode.isMissingNode()) {
final JsonNode modifiedRequestNode = mapper.mapper().createObjectNode().set("native", requestNode);
return xNative.toBuilder()
.request(mapper.encodeToString(modifiedRequestNode))
.build();
}

return xNative;
}


@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {

final List<BidderError> errors = new ArrayList<>();
try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
return Result.withValues(extractBids(bidRequest, bidResponse, errors));
} catch (DecodeException | PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

private List<BidderBid> extractBids(BidRequest bidRequest,
BidResponse bidResponse,
List<BidderError> errors) {
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(TpmnBidder::isValidBid)
.map(bid -> createBidderBid(bid, bidRequest.getImp(), bidResponse.getCur(), errors))
.toList();
}

private static boolean isValidBid(Bid bid) {
return BidderUtil.isValidPrice(ObjectUtil.getIfNotNull(bid, Bid::getPrice));
}

private static BidderBid createBidderBid(Bid bid, List<Imp> imps, String currency, List<BidderError> errors) {
if (StringUtils.isNotEmpty(currency)) {
currency = BIDDER_CURRENCY;
}
final BidType bidType = getBidType(bid, imps);
if (bidType == null) {
errors.add(BidderError.badServerResponse(
"ignoring bid id=%s, request doesn't contain any valid impression with id=%s"
.formatted(bid.getId(), bid.getImpid())));

return null;
}

return BidderBid.of(bid, bidType, currency);
}


private static BidType getBidType(Bid bid, List<Imp> imps) {
final String impId = bid.getImpid();
for (Imp imp : imps) {
if (imp.getId().equals(impId)) {
if (imp.getBanner() != null) {
return BidType.banner;
} else if (imp.getVideo() != null) {
return BidType.video;
} else if (imp.getXNative() != null) {
return BidType.xNative;
}
}
}
throw new PreBidException("Failed to find native/banner/video impression " + impId);
}


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

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.prebid.server.proto.openrtb.ext.request.tpmn;

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

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

@JsonProperty("inventoryId")
Integer inventoryId;

}
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.tpmn.TpmnBidder;
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/tpmn.yaml", factory = YamlPropertySourceFactory.class)
public class TpmnConfiguration {

private static final String BIDDER_NAME = "tpmn";

@Bean("tpmnConfigurationProperties")
@ConfigurationProperties("adapters.tpmn")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

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

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(tpmnConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new TpmnBidder(config.getEndpoint(), mapper, currencyConversionService))
.assemble();
}
}
22 changes: 22 additions & 0 deletions src/main/resources/bidder-config/tpmn.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
adapters:
tpmn:
endpoint: https://gat.tpmn.io/ortb/pbs_bidder
endpoint-compression: gzip
modifyingVastXmlAllowed: true
meta-info:
maintainer-email: [email protected]
app-media-types:
- banner
- video
- native
site-media-types:
- banner
- video
- native
supported-vendors:
vendor-id: 0
usersync:
cookie-family-name: tpmn
redirect:
url: https://gat.tpmn.io/sync/redirect?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir={{redirect_url}}
support-cors: false
16 changes: 16 additions & 0 deletions src/main/resources/static/bidder-params/tpmn.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "TPMN Adapter Params",
"description": "A schema which validates params accepted by the TPMN adapter",
"type": "object",
"properties": {
"inventoryId": {
"description": "Inventory ID",
"type": "integer",
"minLength": 1
}
},
"required": [
"inventoryId"
]
}
Loading