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
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package org.prebid.server.bidder.goadserver;

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.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 io.vertx.core.MultiMap;
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.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.goadserver.ExtImpGoadserver;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
* GoAdserver is a self-hosted, multi-tenant ad server. All requests go to one fixed GoAdserver gateway, which
* routes each to the publisher's deployment by the publisher token. Impressions are grouped by token and each
* group is sent with its token in site.publisher.id, or app.publisher.id for in-app requests.
*/
Comment on lines +41 to +45

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

remove comment

public class GoadserverBidder implements Bidder<BidRequest> {

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

private static final String DEFAULT_CURRENCY = "USD";
private static final String DSA_FIELD = "dsa";

private final String endpointUrl;
private final JacksonMapper mapper;

public GoadserverBidder(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) {
if (request.getSite() == null && request.getApp() == null) {
return Result.withError(BidderError.badInput("goadserver needs a site or an app object"));
}

final List<BidderError> errors = new ArrayList<>();
final Map<String, List<Imp>> impsByToken = new LinkedHashMap<>();

for (Imp imp : request.getImp()) {
try {
final ExtImpGoadserver extImp = parseImpExt(imp);
final String token = validateToken(extImp.getToken(), imp.getId());
impsByToken.computeIfAbsent(token, _ -> new ArrayList<>()).add(modifyImp(imp, extImp));
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}

final List<HttpRequest<BidRequest>> httpRequests = impsByToken.entrySet().stream()
.map(entry -> makeHttpRequest(request, entry.getKey(), entry.getValue()))
.toList();

return Result.of(httpRequests, errors);
}

private ExtImpGoadserver parseImpExt(Imp imp) {
try {
final ExtImpGoadserver extImp = mapper.mapper()
.convertValue(imp.getExt(), GOADSERVER_EXT_TYPE_REFERENCE)
.getBidder();
if (extImp == null) {
throw new PreBidException("imp %s: missing ext.bidder".formatted(imp.getId()));
}
Comment on lines +94 to +96

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can't be null

return extImp;
} catch (IllegalArgumentException e) {
throw new PreBidException("imp %s: invalid ext.bidder".formatted(imp.getId()));
}
}

private static String validateToken(String token, String impId) {
if (StringUtils.isBlank(token)) {
throw new PreBidException("imp %s: missing token".formatted(impId));
}
return token.trim();
}

private Imp modifyImp(Imp imp, ExtImpGoadserver extImp) {
final Imp.ImpBuilder builder = imp.toBuilder().ext(makeImpExt(extImp.getSubid()));

final BigDecimal floor = extImp.getFloor();
if (BidderUtil.isValidPrice(floor) && !BidderUtil.isValidPrice(imp.getBidfloor())) {
builder.bidfloor(floor).bidfloorcur(DEFAULT_CURRENCY);
}
return builder.build();
}

// The GoAdserver endpoint reads an optional sub-identifier from imp.ext.goadserver.subid.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove comment

private ObjectNode makeImpExt(JsonNode subid) {
final String subidValue = subid == null || subid.isNull() ? null : subid.asText();
if (StringUtils.isEmpty(subidValue)) {
return null;
}
final ObjectNode impExt = mapper.mapper().createObjectNode();
impExt.putObject("goadserver").put("subid", subidValue);
return impExt;
}

private HttpRequest<BidRequest> makeHttpRequest(BidRequest request, String token, List<Imp> imps) {
final BidRequest.BidRequestBuilder builder = request.toBuilder().imp(imps);
if (request.getSite() != null) {
builder.site(modifySite(request.getSite(), token));
} else {
builder.app(modifyApp(request.getApp(), token));
}
final BidRequest outgoingRequest = builder.build();
Comment on lines +132 to +138

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

        final BidRequest outgoingRequest = request.toBuilder()
            .imp(imps)
            .site(modifySite(request.getSite(), token))
            .app(modifyApp(request.getApp(), token))
            .build();
private static Site modifySite(Site site, String token) {
        return site != null
               ? site.toBuilder().publisher(withToken(site.getPublisher(), token)).build()
               : null;
    }

    // same for app


final MultiMap headers = HttpUtil.headers()
.add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5");

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

private static Site modifySite(Site site, String token) {
return site.toBuilder().publisher(withToken(site.getPublisher(), token)).build();
}

private static App modifyApp(App app, String token) {
return app.toBuilder().publisher(withToken(app.getPublisher(), token)).build();
}

private static Publisher withToken(Publisher publisher, String token) {
return publisher != null
? publisher.toBuilder().id(token).build()
: Publisher.builder().id(token).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();
}
final String currency = StringUtils.defaultIfEmpty(bidResponse.getCur(), DEFAULT_CURRENCY);
return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.map(bid -> makeBidderBid(bid, currency, errors))
.filter(Objects::nonNull)
.toList();
}

private BidderBid makeBidderBid(Bid bid, String currency, List<BidderError> errors) {
final BidType bidType = getBidType(bid);
if (bidType == null) {
errors.add(BidderError.badServerResponse(
"unsupported media type for bid %s on imp %s".formatted(bid.getId(), bid.getImpid())));
return null;
}
return BidderBid.of(bid.toBuilder().ext(keepDsa(bid.getExt())).build(), bidType, currency);
}

// Prefers bid.mtype and falls back to bid.ext.prebid.type, which GoAdserver sets on every bid.
private static BidType getBidType(Bid bid) {
final BidType byMtype = switch (bid.getMtype()) {
case 1 -> BidType.banner;
case 2 -> BidType.video;
case 4 -> BidType.xNative;
case null, default -> null;
};
if (byMtype != null) {
return byMtype;
}

final ObjectNode ext = bid.getExt();
final String type = ext != null ? ext.path("prebid").path("type").asText(null) : null;
return switch (StringUtils.defaultString(type)) {
case "banner" -> BidType.banner;
case "video" -> BidType.video;
case "native" -> BidType.xNative;
default -> null;
};
}

// Drops the Prebid.js targeting GoAdserver adds for its direct integration and keeps only the
// DSA transparency object, which Prebid Server validates and relays.
Comment on lines +219 to +220

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

remove comment

private ObjectNode keepDsa(ObjectNode ext) {
final JsonNode dsa = ext != null ? ext.get(DSA_FIELD) : null;
if (dsa == null || dsa.isNull()) {
return null;
}
final ObjectNode modifiedExt = mapper.mapper().createObjectNode();
modifiedExt.set(DSA_FIELD, dsa);
return modifiedExt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.prebid.server.proto.openrtb.ext.request.goadserver;

import com.fasterxml.jackson.databind.JsonNode;
import lombok.Value;

import java.math.BigDecimal;

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

String token;

BigDecimal floor;

JsonNode subid;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.prebid.server.spring.config.bidder;

import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.goadserver.GoadserverBidder;
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.env.YamlPropertySourceFactory;
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;

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

private static final String BIDDER_NAME = "goadserver";

@Bean("goadserverConfigurationProperties")
@ConfigurationProperties("adapters.goadserver")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

@Bean
BidderDeps goadserverBidderDeps(BidderConfigurationProperties goadserverConfigurationProperties,
JacksonMapper mapper) {

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(goadserverConfigurationProperties)
.bidderCreator(config -> new GoadserverBidder(config.getEndpoint(), mapper))
.assemble();
}
}
15 changes: 15 additions & 0 deletions src/main/resources/bidder-config/goadserver.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
adapters:
goadserver:
endpoint: https://pbs.goadserver.com/openrtb2/auction
meta-info:
maintainer-email: [email protected]
app-media-types:
- banner
- video
- native
site-media-types:
- banner
- video
- native
supported-vendors:
vendor-id: 0
25 changes: 25 additions & 0 deletions src/main/resources/static/bidder-params/goadserver.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "GoAdserver Adapter Params",
"description": "A schema which validates params accepted by the GoAdserver adapter",
"type": "object",
"properties": {
"token": {
"type": "string",
"description": "Publisher token from the GoAdserver panel. The GoAdserver gateway routes the request to the publisher's deployment by this token",
"minLength": 1,
"maxLength": 128,
"pattern": "^[A-Za-z0-9_-]+$"
},
"floor": {
"type": "number",
"description": "Bid floor in USD CPM, applied when the impression has no bidfloor of its own",
"minimum": 0
},
"subid": {
"type": ["string", "integer"],
"description": "Sub-identifier for stats attribution (page section, A/B group, etc.)"
}
},
"required": ["token"]
}
Loading