Skip to content

Commit edc2453

Browse files
New Adapter: Nativery (#4223)
1 parent 4f64c15 commit edc2453

File tree

13 files changed

+1088
-0
lines changed

13 files changed

+1088
-0
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
package org.prebid.server.bidder.nativery;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.JsonNode;
5+
import com.fasterxml.jackson.databind.node.ObjectNode;
6+
import com.iab.openrtb.request.BidRequest;
7+
import com.iab.openrtb.request.Imp;
8+
import com.iab.openrtb.response.Bid;
9+
import com.iab.openrtb.response.BidResponse;
10+
import com.iab.openrtb.response.SeatBid;
11+
import org.apache.commons.collections4.CollectionUtils;
12+
import org.apache.commons.collections4.ListUtils;
13+
import org.apache.commons.lang3.StringUtils;
14+
import org.prebid.server.auction.model.Endpoint;
15+
import org.prebid.server.bidder.Bidder;
16+
import org.prebid.server.bidder.model.BidderBid;
17+
import org.prebid.server.bidder.model.BidderCall;
18+
import org.prebid.server.bidder.model.BidderError;
19+
import org.prebid.server.bidder.model.HttpRequest;
20+
import org.prebid.server.bidder.model.HttpResponse;
21+
import org.prebid.server.bidder.model.Result;
22+
import org.prebid.server.exception.PreBidException;
23+
import org.prebid.server.json.DecodeException;
24+
import org.prebid.server.json.JacksonMapper;
25+
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
26+
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
27+
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
28+
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidServer;
29+
import org.prebid.server.proto.openrtb.ext.request.nativery.BidExtNativery;
30+
import org.prebid.server.proto.openrtb.ext.request.nativery.ExtImpNativery;
31+
import org.prebid.server.proto.openrtb.ext.response.BidType;
32+
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
33+
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta;
34+
import org.prebid.server.util.BidderUtil;
35+
import org.prebid.server.util.HttpUtil;
36+
37+
import java.util.ArrayList;
38+
import java.util.Collection;
39+
import java.util.Collections;
40+
import java.util.List;
41+
import java.util.Objects;
42+
import java.util.Optional;
43+
44+
public class NativeryBidder implements Bidder<BidRequest> {
45+
46+
private static final TypeReference<ExtPrebid<?, ExtImpNativery>> NATIVERY_EXT_TYPE_REFERENCE =
47+
new TypeReference<>() {
48+
};
49+
50+
private static final TypeReference<ExtPrebid<ExtBidPrebid, ?>> EXT_PREBID_TYPE_REFERENCE =
51+
new TypeReference<>() {
52+
};
53+
54+
private static final String DEFAULT_CURRENCY = "EUR";
55+
56+
private final String endpointUrl;
57+
private final JacksonMapper mapper;
58+
59+
public NativeryBidder(String endpointUrl, JacksonMapper mapper) {
60+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
61+
this.mapper = Objects.requireNonNull(mapper);
62+
}
63+
64+
@Override
65+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
66+
final List<HttpRequest<BidRequest>> httpRequests = new ArrayList<>();
67+
final List<BidderError> errors = new ArrayList<>();
68+
69+
final String requestEndpointName = extractEndpointName(request);
70+
final boolean isAmp = StringUtils.equals(requestEndpointName, Endpoint.openrtb2_amp.value());
71+
72+
final List<Imp> validImps = new ArrayList<>();
73+
String widgetId = null;
74+
75+
for (Imp imp : request.getImp()) {
76+
try {
77+
final ExtImpNativery extImp = parseImpExt(imp);
78+
if (widgetId == null && StringUtils.isNotBlank(extImp.getWidgetId())) {
79+
widgetId = extImp.getWidgetId();
80+
}
81+
validImps.add(imp);
82+
} catch (PreBidException e) {
83+
errors.add(BidderError.badInput(e.getMessage()));
84+
}
85+
}
86+
87+
if (validImps.isEmpty()) {
88+
return Result.withErrors(errors);
89+
}
90+
91+
final ExtRequest updatedExt = buildRequestExtWithNativery(request.getExt(), isAmp, widgetId);
92+
93+
for (Imp imp : validImps) {
94+
final BidRequest singleImpRequest = request.toBuilder()
95+
.imp(Collections.singletonList(imp))
96+
.ext(updatedExt)
97+
.build();
98+
99+
httpRequests.add(BidderUtil.defaultRequest(singleImpRequest, endpointUrl, mapper));
100+
}
101+
102+
return Result.of(httpRequests, errors);
103+
}
104+
105+
private static String extractEndpointName(BidRequest bidRequest) {
106+
return Optional.ofNullable(bidRequest)
107+
.map(BidRequest::getExt)
108+
.map(ExtRequest::getPrebid)
109+
.map(ExtRequestPrebid::getServer)
110+
.map(ExtRequestPrebidServer::getEndpoint)
111+
.orElse(null);
112+
}
113+
114+
private ExtImpNativery parseImpExt(Imp imp) {
115+
try {
116+
return mapper.mapper().convertValue(imp.getExt(), NATIVERY_EXT_TYPE_REFERENCE).getBidder();
117+
} catch (IllegalArgumentException e) {
118+
throw new PreBidException("Failed to deserialize Nativery extension: " + e.getMessage());
119+
}
120+
}
121+
122+
private ExtRequest buildRequestExtWithNativery(ExtRequest originalExt, boolean isAmp, String widgetId) {
123+
final ExtRequest ext = originalExt != null ? originalExt : ExtRequest.empty();
124+
125+
final JsonNode existing = ext.getProperty("nativery");
126+
final ObjectNode nativeryNode = existing != null && existing.isObject()
127+
? (ObjectNode) existing
128+
: mapper.mapper().createObjectNode();
129+
130+
nativeryNode.put("isAmp", isAmp);
131+
if (StringUtils.isNotBlank(widgetId)) {
132+
nativeryNode.put("widgetId", widgetId);
133+
}
134+
135+
ext.addProperty("nativery", nativeryNode);
136+
return ext;
137+
}
138+
139+
@Override
140+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
141+
final List<BidderError> errors = new ArrayList<>();
142+
143+
final HttpResponse response = httpCall.getResponse();
144+
145+
try {
146+
final BidResponse bidResponse = mapper.decodeValue(response.getBody(), BidResponse.class);
147+
final List<BidderBid> bidderBids = extractBids(bidResponse, errors);
148+
return Result.of(bidderBids, errors);
149+
} catch (DecodeException e) {
150+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
151+
}
152+
}
153+
154+
private List<BidderBid> extractBids(BidResponse bidResponse, List<BidderError> errors) {
155+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
156+
return Collections.emptyList();
157+
}
158+
159+
final String currency = StringUtils.defaultIfBlank(bidResponse.getCur(), DEFAULT_CURRENCY);
160+
161+
return bidResponse.getSeatbid().stream()
162+
.filter(Objects::nonNull)
163+
.map(SeatBid::getBid)
164+
.filter(Objects::nonNull)
165+
.flatMap(Collection::stream)
166+
.filter(Objects::nonNull)
167+
.map(bid -> resolveBidderBid(bid, currency, errors))
168+
.filter(Objects::nonNull)
169+
.toList();
170+
}
171+
172+
private BidderBid resolveBidderBid(Bid bid, String currency, List<BidderError> errors) {
173+
try {
174+
final BidExtNativery nativeryExt = parseNativeryExt(bid.getExt());
175+
176+
final BidType bidType = mapMediaType(nativeryExt.getBidAdMediaType());
177+
final List<String> advDomains = ListUtils.emptyIfNull(
178+
nativeryExt.getBidAdvDomains());
179+
180+
final Bid updatedBid = addBidMeta(bid, bidType.getName(), advDomains);
181+
return BidderBid.of(updatedBid, bidType, currency);
182+
} catch (PreBidException e) {
183+
errors.add(BidderError.badInput(e.getMessage()));
184+
return null;
185+
}
186+
}
187+
188+
private BidExtNativery parseNativeryExt(ObjectNode bidExt) {
189+
return Optional.ofNullable(bidExt)
190+
.map(ext -> ext.get("nativery"))
191+
.filter(JsonNode::isObject)
192+
.map(this::toBidExtNativery)
193+
.orElseThrow(() -> new PreBidException("missing bid.ext.nativery"));
194+
}
195+
196+
private BidExtNativery toBidExtNativery(JsonNode node) {
197+
try {
198+
return mapper.mapper().convertValue(node, BidExtNativery.class);
199+
} catch (IllegalArgumentException e) {
200+
throw new PreBidException("invalid bid.ext.nativery: " + e.getMessage());
201+
}
202+
}
203+
204+
private static BidType mapMediaType(String mediaType) {
205+
final String mt = StringUtils.defaultString(mediaType).toLowerCase();
206+
return switch (mt) {
207+
case "native" -> BidType.xNative;
208+
case "display", "banner", "rich_media" -> BidType.banner;
209+
case "video" -> BidType.video;
210+
default -> throw new PreBidException(
211+
"unrecognized bid_ad_media_type in response from nativery: " + mediaType);
212+
};
213+
}
214+
215+
private Bid addBidMeta(Bid bid, String mediaType, List<String> advDomains) {
216+
final ExtBidPrebid prebid = parseExtBidPrebid(bid);
217+
218+
final ExtBidPrebidMeta modifiedMeta = Optional.ofNullable(prebid)
219+
.map(ExtBidPrebid::getMeta)
220+
.map(ExtBidPrebidMeta::toBuilder)
221+
.orElseGet(ExtBidPrebidMeta::builder)
222+
.mediaType(mediaType)
223+
.advertiserDomains(advDomains)
224+
.build();
225+
226+
final ExtBidPrebid modifiedPrebid = Optional.ofNullable(prebid)
227+
.map(ExtBidPrebid::toBuilder)
228+
.orElseGet(ExtBidPrebid::builder)
229+
.meta(modifiedMeta)
230+
.build();
231+
232+
return bid.toBuilder()
233+
.ext(mapper.mapper().valueToTree(ExtPrebid.of(modifiedPrebid, null)))
234+
.build();
235+
}
236+
237+
private ExtBidPrebid parseExtBidPrebid(Bid bid) {
238+
try {
239+
return mapper.mapper()
240+
.convertValue(bid.getExt(), EXT_PREBID_TYPE_REFERENCE)
241+
.getPrebid();
242+
} catch (IllegalArgumentException e) {
243+
throw new PreBidException("Failed to deserialize Prebid extension: " + e.getMessage());
244+
}
245+
}
246+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.prebid.server.proto.openrtb.ext.request.nativery;
2+
3+
import lombok.Value;
4+
5+
import java.util.List;
6+
7+
@Value(staticConstructor = "of")
8+
public class BidExtNativery {
9+
10+
String bidAdMediaType;
11+
12+
List<String> bidAdvDomains;
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.prebid.server.proto.openrtb.ext.request.nativery;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Value;
5+
6+
@Value(staticConstructor = "of")
7+
public class ExtImpNativery {
8+
9+
@JsonProperty("widgetId")
10+
String widgetId;
11+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.prebid.server.spring.config.bidder;
2+
3+
import org.prebid.server.bidder.BidderDeps;
4+
import org.prebid.server.bidder.nativery.NativeryBidder;
5+
import org.prebid.server.json.JacksonMapper;
6+
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
7+
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
8+
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
9+
import org.prebid.server.spring.env.YamlPropertySourceFactory;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.boot.context.properties.ConfigurationProperties;
12+
import org.springframework.context.annotation.Bean;
13+
import org.springframework.context.annotation.Configuration;
14+
import org.springframework.context.annotation.PropertySource;
15+
16+
import jakarta.validation.constraints.NotBlank;
17+
18+
@Configuration
19+
@PropertySource(value = "classpath:/bidder-config/nativery.yaml", factory = YamlPropertySourceFactory.class)
20+
public class NativeryBidderConfiguration {
21+
22+
private static final String BIDDER_NAME = "nativery";
23+
24+
@Bean("nativeryConfigurationProperties")
25+
@ConfigurationProperties("adapters.nativery")
26+
BidderConfigurationProperties configurationProperties() {
27+
return new BidderConfigurationProperties();
28+
}
29+
30+
@Bean
31+
BidderDeps nativeryBidderDeps(BidderConfigurationProperties nativeryConfigurationProperties,
32+
@NotBlank @Value("${external-url}") String externalUrl,
33+
JacksonMapper mapper) {
34+
35+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
36+
.withConfig(nativeryConfigurationProperties)
37+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
38+
.bidderCreator(config -> new NativeryBidder(config.getEndpoint(), mapper))
39+
.assemble();
40+
}
41+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
adapters:
2+
nativery:
3+
endpoint: "https://hb.nativery.com/openrtb2/auction"
4+
ortb-version: "2.6"
5+
meta-info:
6+
maintainer-email: "[email protected]"
7+
site-media-types:
8+
- banner
9+
- video
10+
- native
11+
supported-vendors:
12+
vendor-id: 1133
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Nativery Adapter Params",
4+
"description": "A schema which validates params accepted by the Nativery adapter",
5+
6+
"type": "object",
7+
"properties": {
8+
"widgetId": {
9+
"type": "string",
10+
"description": "An ID which identifies this Nativery widget"
11+
}
12+
},
13+
14+
"required": ["widgetId"]
15+
}

0 commit comments

Comments
 (0)