Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Orbidder bidder #807

Merged
merged 4 commits into from
Sep 15, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
131 changes: 131 additions & 0 deletions src/main/java/org/prebid/server/bidder/orbidder/OrbidderBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.prebid.server.bidder.orbidder;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.http.HttpMethod;
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.BidderError;
import org.prebid.server.bidder.model.HttpCall;
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.orbidder.ExtImpOrbidder;
import org.prebid.server.proto.openrtb.ext.response.BidType;
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.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class OrbidderBidder implements Bidder<BidRequest> {

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

private static final String DEFAULT_BID_CURRENCY = "USD";

private final String endpointUrl;
private final JacksonMapper mapper;

public OrbidderBidder(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 List<Imp> validImps = new ArrayList<>();

if (CollectionUtils.isEmpty(request.getImp())) {
AndriyPavlyuk marked this conversation as resolved.
Show resolved Hide resolved
errors.add(BidderError.badInput("No valid impressions in the bid request"));
return Result.of(Collections.emptyList(), errors);
}

for (Imp imp : request.getImp()) {
try {
parseImpExt(imp);
validImps.add(imp);
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}
final BidRequest outgoingRequest = request.toBuilder().imp(validImps).build();
final String body = mapper.encode(outgoingRequest);

return Result.of(Collections.singletonList(
HttpRequest.<BidRequest>builder()
.method(HttpMethod.POST)
.uri(endpointUrl)
.headers(HttpUtil.headers())
.payload(outgoingRequest)
.body(body)
.build()),
errors);
}

private void parseImpExt(Imp imp) {
try {
mapper.mapper().convertValue(imp.getExt(), ORBIDDER_EXT_TYPE_REFERENCE);
} catch (IllegalArgumentException e) {
throw new PreBidException(e.getMessage(), e);
}
}

@Override
public Result<List<BidderBid>> makeBids(HttpCall<BidRequest> httpCall, BidRequest bidRequest) {
final int statusCode = httpCall.getResponse().getStatusCode();
if (statusCode == HttpResponseStatus.NO_CONTENT.code()) {
return Result.of(Collections.emptyList(), Collections.emptyList());
} else if (statusCode == HttpResponseStatus.BAD_REQUEST.code()) {
return Result.emptyWithError(BidderError.badInput("Invalid request."));
} else if (statusCode == HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) {
return Result.emptyWithError(BidderError.badInput("Server internal error."));
} else if (statusCode != HttpResponseStatus.OK.code()) {
return Result.emptyWithError(BidderError.badServerResponse(String.format("Unexpected HTTP status %s.",
statusCode)));
}

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

final List<BidderBid> bidderBids = bidResponse.getSeatbid().stream()
.map(SeatBid::getBid)
.flatMap(Collection::stream)
.map(bid -> BidderBid.of(bid, BidType.banner, DEFAULT_BID_CURRENCY))
.collect(Collectors.toList());
return Result.of(bidderBids, Collections.emptyList());
}

private BidResponse decodeBodyToBidResponse(HttpCall<BidRequest> httpCall) {
try {
return mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
} catch (DecodeException e) {
throw new PreBidException(e.getMessage(), e);
}
}

@Override
public Map<String, String> extractTargeting(ObjectNode ext) {
return Collections.emptyMap();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.prebid.server.proto.openrtb.ext.request.orbidder;

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

import java.math.BigDecimal;

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

@JsonProperty("accountId")
String accountId;

@JsonProperty("placementId")
String placementId;

@JsonProperty("bidfloor")
AndriyPavlyuk marked this conversation as resolved.
Show resolved Hide resolved
BigDecimal bidFloor;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.prebid.server.spring.config.bidder;

import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.orbidder.OrbidderBidder;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
import org.prebid.server.spring.env.YamlPropertySourceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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/orbidder.yaml", factory = YamlPropertySourceFactory.class)
public class OrbidderConfiguration {

private static final String BIDDER_NAME = "orbidder";

@Value("${external-url}")
@NotBlank
private String externalUrl;

@Autowired
private JacksonMapper mapper;

@Autowired
@Qualifier("orbidderConfigurationProperties")
private BidderConfigurationProperties configProperties;

@Bean("orbidderConfigurationProperties")
@ConfigurationProperties("adapters.orbidder")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

@Bean
BidderDeps orbidderBidderDeps() {
final UsersyncConfigurationProperties usersync = configProperties.getUsersync();

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(configProperties)
.bidderInfo(BidderInfoCreator.create(configProperties))
.usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
.bidderCreator(() -> new OrbidderBidder(configProperties.getEndpoint(), mapper))
.assemble();
}
}
23 changes: 23 additions & 0 deletions src/main/resources/bidder-config/orbidder.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
adapters:
orbidder:
enabled: false
endpoint: https://orbidder.otto.de/openrtb2
pbs-enforces-gdpr: true
pbs-enforces-ccpa: true
modifying-vast-xml-allowed: true
deprecated-names:
aliases:
meta-info:
maintainer-email: realtime-siggi@otto.de
app-media-types:
- banner
site-media-types:
- banner
supported-vendors:
vendor-id: 0
usersync:
url:
redirect-url:
cookie-family-name: orbidder
type: redirect
support-cors: false
24 changes: 24 additions & 0 deletions src/main/resources/static/bidder-params/orbidder.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Orbidder Adapter Params",
"description": "A schema which validates params accepted by the Orbidder adapter",

"type": "object",
"properties": {
"accountId": {
"type": "string",
"description": "The marketer's accountId."
},
"placementId": {
"type": "string",
"description": "The placementId of the ad unit."
},
"bidfloor": {
"type": "number",
"description": "The minimum CPM price in EUR.",
"minimum": 0
}
},

"required": ["accountId", "placementId"]
}
Loading