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
240 changes: 240 additions & 0 deletions src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package org.prebid.server.bidder.goldbach;

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.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.prebid.server.bidder.Bidder;
import org.prebid.server.bidder.goldbach.proto.GoldbachExtImp;
import org.prebid.server.bidder.goldbach.proto.ExtRequestGoldbach;
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.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.goldbach.ExtImpGoldbach;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;

public class GoldbachBidder implements Bidder<BidRequest> {

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

private static final TypeReference<ExtPrebid<ExtBidPrebid, ObjectNode>> EXT_PREBID_TYPE_REFERENCE =
new TypeReference<>() {
};

private final String endpointUrl;
private final JacksonMapper mapper;

public GoldbachBidder(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<BidderError> errors = new ArrayList<>();

final ExtRequestGoldbach extRequestGoldbach = parseRequestExt(request, errors);
final Map<String, List<Imp>> publisherToImps = new HashMap<>();
for (Imp imp : request.getImp()) {
try {
final ExtImpGoldbach extImp = parseImpExt(imp);
final Imp updatedImp = modifyImp(imp, extImp);

publisherToImps.computeIfAbsent(extImp.getPublisherId(), k -> new ArrayList<>())
.add(updatedImp);
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}

if (publisherToImps.isEmpty()) {
errors.add(BidderError.badInput("No valid impressions found"));
}

final List<HttpRequest<BidRequest>> httpRequests = publisherToImps.entrySet().stream()
.map(publisherIdAndImps -> makeHttpRequestForPublisher(
request,
extRequestGoldbach,
publisherIdAndImps.getKey(),
publisherIdAndImps.getValue()))
.filter(Objects::nonNull)
.toList();

return Result.of(httpRequests, errors);
}

private ExtRequestGoldbach parseRequestExt(BidRequest bidRequest, List<BidderError> errors) {
return Optional.ofNullable(bidRequest.getExt())
.map(extRequest -> extRequest.getProperty("goldbach"))
.map(extRequestGoldbachRaw -> parseRequestExtGoldbach(extRequestGoldbachRaw, errors))
.orElse(null);
}

private ExtRequestGoldbach parseRequestExtGoldbach(JsonNode extRequestGoldbachRaw, List<BidderError> errors) {
try {
return mapper.mapper().treeToValue(extRequestGoldbachRaw, ExtRequestGoldbach.class);
} catch (IllegalArgumentException | JsonProcessingException e) {
errors.add(BidderError.badInput("Failed to deserialize Goldbach bid request extension: " + e.getMessage()));
return null;
}
}

private ExtImpGoldbach parseImpExt(Imp imp) {
try {
return mapper.mapper().convertValue(imp.getExt(), GOLDBACH_EXT_TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException("Failed to deserialize Goldbach imp extension: " + e.getMessage());
}
}

private Imp modifyImp(Imp imp, ExtImpGoldbach extImp) {
return imp.toBuilder()
.ext(mapper.mapper().createObjectNode().set("goldbach", mapper.mapper().valueToTree(
GoldbachExtImp.of(extImp.getSlotId(), extImp.getCustomTargeting()))))
.build();
}

private HttpRequest<BidRequest> makeHttpRequestForPublisher(BidRequest bidRequest,
ExtRequestGoldbach extRequestGoldbach,
String publisherId,
List<Imp> imps) {

final BidRequest modifiedBidRequest = modifyBidRequest(bidRequest, extRequestGoldbach, publisherId, imps);
return BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper);
}

private BidRequest modifyBidRequest(BidRequest bidRequest,
ExtRequestGoldbach extRequestGoldbach,
String publisherId,
List<Imp> imps) {

final ExtRequest modifiedExtRequest = modifyExtRequest(bidRequest.getExt(), extRequestGoldbach, publisherId);
return bidRequest.toBuilder()
.id("%s_%s".formatted(bidRequest.getId(), publisherId))
.imp(imps)
.ext(modifiedExtRequest)
.build();
}

private ExtRequest modifyExtRequest(ExtRequest extRequest,
ExtRequestGoldbach extRequestGoldbach,
String publisherId) {

final ExtRequestGoldbach modifiedExtRequestGoldbach = ExtRequestGoldbach.of(
publisherId,
extRequestGoldbach != null ? extRequestGoldbach.getMockResponse() : null);

final ExtRequest modifiedExtRequest;

if (extRequest != null) {
modifiedExtRequest = ExtRequest.of(extRequest.getPrebid());
mapper.fillExtension(modifiedExtRequest, extRequest);
} else {
modifiedExtRequest = ExtRequest.empty();
}

modifiedExtRequest.addProperty("goldbach", mapper.mapper().valueToTree(modifiedExtRequestGoldbach));
return modifiedExtRequest;
}

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
if (httpCall.getResponse().getStatusCode() != HttpResponseStatus.CREATED.code()) {
return Result.withError(
BidderError.badServerResponse(
"unexpected status code: %d. Run with request.debug = 1 for more info".formatted(
httpCall.getResponse().getStatusCode())));
}

final BidResponse bidResponse;
try {
bidResponse = decodeBodyToBidResponse(httpCall);
} catch (PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}

return bidsFromResponse(bidResponse);
}

private BidResponse decodeBodyToBidResponse(BidderCall<BidRequest> httpCall) {
try {
return mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
} catch (DecodeException e) {
throw new PreBidException("Failed to parse response as BidResponse: " + e.getMessage());
}
}

private Result<List<BidderBid>> bidsFromResponse(BidResponse bidResponse) {
final List<BidderError> errors = new ArrayList<>();
final List<BidderBid> bidderBids = Stream.ofNullable(bidResponse)
.map(BidResponse::getSeatbid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.map(bid -> makeBid(bid, bidResponse, errors))
.filter(Objects::nonNull)
.toList();

if (bidderBids.isEmpty()) {
errors.add(BidderError.badServerResponse("No valid bids found in response"));
}

return Result.of(bidderBids, errors);
}

private BidderBid makeBid(Bid bid, BidResponse bidResponse, List<BidderError> errors) {
try {
return BidderBid.of(bid, getBidType(bid), bidResponse.getCur());
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
return null;
}
}

private BidType getBidType(Bid bid) {
return Optional.ofNullable(bid.getExt())
.map(this::parseBidExt)
.map(ExtPrebid::getPrebid)
.map(ExtBidPrebid::getType)
.orElseThrow(() -> new PreBidException("No media type for bid " + bid.getId()));
}

private ExtPrebid<ExtBidPrebid, ObjectNode> parseBidExt(ObjectNode bidExt) {
try {
return mapper.mapper().convertValue(bidExt, EXT_PREBID_TYPE_REFERENCE);
} catch (IllegalArgumentException e) {
throw new PreBidException("Failed to deserialize ext for bid: " + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.prebid.server.bidder.goldbach.proto;

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

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

@JsonProperty("publisherId")
String publisherId;

@JsonProperty("mockResponse")
Boolean mockResponse;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.prebid.server.bidder.goldbach.proto;

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

import java.util.List;
import java.util.Map;

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

@JsonProperty("slotId")
String slotId;

Map<String, List<String>> targetings;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.prebid.server.proto.openrtb.ext.request.goldbach;

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

import java.util.List;
import java.util.Map;

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

@JsonProperty("publisherId")
String publisherId;

@JsonProperty("slotId")
String slotId;

@JsonProperty("customTargeting")
Map<String, List<String>> customTargeting;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.prebid.server.spring.config.bidder;

import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.goldbach.GoldbachBidder;
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/goldbach.yaml", factory = YamlPropertySourceFactory.class)
public class GoldbachConfiguration {

private static final String BIDDER_NAME = "goldbach";

@Bean("goldbachConfigurationProperties")
@ConfigurationProperties("adapters.goldbach")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

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

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(goldbachConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new GoldbachBidder(config.getEndpoint(), mapper))
.assemble();
}
}
16 changes: 16 additions & 0 deletions src/main/resources/bidder-config/goldbach.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
adapters:
goldbach:
endpoint: https://goldlayer-api.prod.gbads.net/openrtb/2.5/auction
endpoint-compression: gzip
meta-info:
maintainer-email: engineering@goldbach.com
app-media-types:
- banner
- video
- native
site-media-types:
- banner
- video
- native
supported-vendors:
vendor-id: 580
39 changes: 39 additions & 0 deletions src/main/resources/static/bidder-params/goldbach.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Goldbach Adapter Params",
"description": "A schema which validates params accepted by the Goldbach adapter",
"type": "object",
"properties": {
"publisherId": {
"type": "string",
"minLength": 1,
"description": "Publisher Environment ID"
},
"slotId": {
"type": "string",
"minLength": 1,
"description": "Slot Id"
},
"customTargeting": {
"type": "object",
"description": "Custom Targeting Parameters",
"additionalProperties": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
}
},
"required": [
"publisherId",
"slotId"
]
}
Loading
Loading