Skip to content
Merged
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
180 changes: 180 additions & 0 deletions src/main/java/org/prebid/server/bidder/akcelo/AkceloBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package org.prebid.server.bidder.akcelo;

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.ObjectNode;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.request.Publisher;
import com.iab.openrtb.request.Site;
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.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.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.ExtPublisher;
import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid;
import org.prebid.server.proto.openrtb.ext.request.akcelo.ExtImpAkcelo;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
import org.prebid.server.util.BidderUtil;
import org.prebid.server.util.HttpUtil;

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 AkceloBidder implements Bidder<BidRequest> {

private static final TypeReference<ExtPrebid<?, ExtImpAkcelo>> AKCELO_EXT_TYPE_REFERENCE =
new TypeReference<>() {
};
private static final String BIDDER_NAME = "akcelo";

private final String endpointUrl;
private final JacksonMapper mapper;

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

@Override
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
final List<Imp> imps = request.getImp();
final List<Imp> modifiedImps = new ArrayList<>();

final ExtImpAkcelo firstExtImp;
try {
firstExtImp = parseImpExt(imps.getFirst());
} catch (PreBidException e) {
return Result.withError(BidderError.badInput(e.getMessage()));
}

for (final Imp imp : imps) {
modifiedImps.add(modifyImp(imp));
}

final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, firstExtImp.getSiteId());
return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper));
}

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

private Imp modifyImp(Imp imp) {
return imp.toBuilder()
.ext(mapper.mapper().createObjectNode().set(BIDDER_NAME, imp.getExt().get("bidder")))
.build();
}

private BidRequest modifyRequest(BidRequest request, List<Imp> imps, String siteId) {
return request.toBuilder()
.imp(imps)
.site(modifySite(request.getSite(), siteId))
.build();
}

private Site modifySite(Site site, String siteId) {
final Publisher publisher = Optional.ofNullable(site)
.map(Site::getPublisher)
.map(Publisher::toBuilder)
.orElseGet(Publisher::builder)
.ext(ExtPublisher.of(ExtPublisherPrebid.of(siteId)))
.build();

return Optional.ofNullable(site)
.map(Site::toBuilder)
.orElseGet(Site::builder)
.publisher(publisher)
.build();
}

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
final List<BidderError> errors = new ArrayList<>();
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 bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.map(bid -> makeBid(bid, bidResponse.getCur(), errors))
.filter(Objects::nonNull)
.toList();
}

private BidderBid makeBid(Bid bid, String currency, List<BidderError> errors) {
final BidType bidType = getBidType(bid, errors);
return bidType == null ? null : BidderBid.of(bid, bidType, currency);
}

private BidType getBidType(Bid bid, List<BidderError> errors) {
final Integer mType = bid.getMtype();
if (mType != null) {
return switch (mType) {
case 1 -> BidType.banner;
case 2 -> BidType.video;
case 4 -> BidType.xNative;
default -> {
errors.add(BidderError.badServerResponse("unable to get media type " + mType));
yield null;
}
};
}

return getExtBidPrebidType(bid, errors);
}

private BidType getExtBidPrebidType(Bid bid, List<BidderError> errors) {
return Optional.ofNullable(bid.getExt())
.map(ext -> ext.get("prebid"))
.filter(JsonNode::isObject)
.map(ObjectNode.class::cast)
.map(this::parseExtBidPrebid)
.map(ExtBidPrebid::getType)
.orElseGet(() -> {
errors.add(BidderError.badServerResponse("missing media type for bid " + bid.getId()));
return null;
});
}

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,16 @@
package org.prebid.server.proto.openrtb.ext.request.akcelo;

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

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

@JsonProperty("adUnitId")
Integer adUnitId;

@JsonProperty("siteId")
String siteId;

Integer 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.akcelo.AkceloBidder;
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/akcelo.yaml",
factory = YamlPropertySourceFactory.class)
public class AkceloConfiguration {

private static final String BIDDER_NAME = "akcelo";

@Bean("akceloConfigurationProperties")
@ConfigurationProperties("adapters.akcelo")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

@Bean
BidderDeps akceloBidderDeps(BidderConfigurationProperties akceloConfigurationProperties,
@NotBlank @Value("${external-url}") String externalUrl,
JacksonMapper mapper) {

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(akceloConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new AkceloBidder(config.getEndpoint(), mapper))
.assemble();
}

}
12 changes: 12 additions & 0 deletions src/main/resources/bidder-config/akcelo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
adapters:
akcelo:
endpoint: https://s2s.sportslocalmedia.com/openrtb2/auction
meta-info:
maintainer-email: tech@akcelo.io
app-media-types:
site-media-types:
- banner
- video
- native
supported-vendors:
vendor-id: 0
24 changes: 24 additions & 0 deletions src/main/resources/static/bidder-params/akcelo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Akcelo Adapter Params",
"description": "A schema which validates params accepted by the Akcelo adapter",
"type": "object",
"properties": {
"adUnitID": {
"type": "number",
"description": "The identifier of the ad unit. Will be provided by your account manager."
},
"siteId": {
"type": "number",
"description": "The identifier of the site. Will be provided by your account manager."
},
"test": {
"type": "number",
"description": "Whether to display test creatives or not. Default is 0."
}
},
"required": [
"adUnitId",
"siteId"
]
}
Loading
Loading