1010import io .vertx .core .http .HttpServerRequest ;
1111import io .vertx .core .http .HttpServerResponse ;
1212import io .vertx .ext .web .RoutingContext ;
13+ import lombok .Value ;
1314import org .apache .commons .lang3 .BooleanUtils ;
1415import org .apache .commons .lang3 .ObjectUtils ;
1516import org .apache .commons .lang3 .StringUtils ;
16- import org .apache .commons .lang3 .tuple .Pair ;
1717import org .prebid .server .activity .Activity ;
1818import org .prebid .server .activity .ComponentType ;
1919import org .prebid .server .activity .infrastructure .ActivityInfrastructure ;
5454import org .prebid .server .settings .model .AccountGdprConfig ;
5555import org .prebid .server .settings .model .AccountPrivacyConfig ;
5656import org .prebid .server .util .HttpUtil ;
57- import org .prebid .server .util .StreamUtil ;
5857import org .prebid .server .vertx .verticles .server .HttpEndpoint ;
5958import org .prebid .server .vertx .verticles .server .application .ApplicationResource ;
6059
6160import java .util .Collections ;
62- import java .util .Comparator ;
6361import java .util .List ;
6462import java .util .Map ;
6563import java .util .Objects ;
6664import java .util .Optional ;
65+ import java .util .Set ;
6766import java .util .function .Consumer ;
68- import java .util .function .Function ;
69- import java .util .function .Predicate ;
7067import java .util .stream .Collectors ;
7168
7269public class SetuidHandler implements ApplicationResource {
@@ -89,7 +86,7 @@ public class SetuidHandler implements ApplicationResource {
8986 private final AnalyticsReporterDelegator analyticsDelegator ;
9087 private final Metrics metrics ;
9188 private final TimeoutFactory timeoutFactory ;
92- private final Map <String , Pair < String , UsersyncMethodType >> cookieNameToBidderAndSyncType ;
89+ private final Map <String , UsersyncConfig > cookieFamilyNameToUsersyncConfig ;
9390
9491 public SetuidHandler (long defaultTimeout ,
9592 UidsCookieService uidsCookieService ,
@@ -113,49 +110,58 @@ public SetuidHandler(long defaultTimeout,
113110 this .analyticsDelegator = Objects .requireNonNull (analyticsDelegator );
114111 this .metrics = Objects .requireNonNull (metrics );
115112 this .timeoutFactory = Objects .requireNonNull (timeoutFactory );
116- this .cookieNameToBidderAndSyncType = collectUsersyncers (bidderCatalog );
113+ this .cookieFamilyNameToUsersyncConfig = collectUserSyncConfigs (bidderCatalog );
117114 }
118115
119- private static Map <String , Pair <String , UsersyncMethodType >> collectUsersyncers (BidderCatalog bidderCatalog ) {
120- validateUsersyncersDuplicates (bidderCatalog );
121-
116+ private static Map <String , UsersyncConfig > collectUserSyncConfigs (BidderCatalog bidderCatalog ) {
122117 return bidderCatalog .usersyncReadyBidders ().stream ()
123- .sorted (Comparator .comparing (bidderName -> BooleanUtils .toInteger (bidderCatalog .isAlias (bidderName ))))
124- .filter (StreamUtil .distinctBy (bidderCatalog ::cookieFamilyName ))
125- .map (bidderName -> bidderCatalog .usersyncerByName (bidderName )
126- .map (usersyncer -> Pair .of (bidderName , usersyncer )))
127- .flatMap (Optional ::stream )
128- .collect (Collectors .toMap (
129- pair -> pair .getRight ().getCookieFamilyName (),
130- pair -> Pair .of (pair .getLeft (), preferredUserSyncType (pair .getRight ()))));
118+ .collect (Collectors .groupingBy (
119+ bidderName -> bidderCatalog .cookieFamilyName (bidderName ).orElseThrow (),
120+ Collectors .collectingAndThen (Collectors .toSet (), bidderNames ->
121+ usersyncConfigForSingleCookieFamilyName (bidderNames , bidderCatalog ))));
131122 }
132123
133- private static void validateUsersyncersDuplicates (BidderCatalog bidderCatalog ) {
134- final List <String > duplicatedCookieFamilyNames = bidderCatalog .usersyncReadyBidders ().stream ()
135- .filter (bidderName -> !isAliasWithRootCookieFamilyName (bidderCatalog , bidderName ))
124+ private static UsersyncConfig usersyncConfigForSingleCookieFamilyName (Set <String > bidderNames ,
125+ BidderCatalog bidderCatalog ) {
126+
127+ final Set <String > biddersWithoutAliases = bidderNames .stream ()
128+ .filter (bidderName -> bidderCatalog .aliasOf (bidderName ).filter (bidderNames ::contains ).isEmpty ())
129+ .collect (Collectors .toSet ());
130+
131+ validateBiddersHaveTheSameVendorId (biddersWithoutAliases , bidderCatalog );
132+ validateBiddersHaveTheSameUsersyncConfig (biddersWithoutAliases , bidderCatalog );
133+
134+ final Usersyncer usersyncer = biddersWithoutAliases .stream ()
136135 .map (bidderCatalog ::usersyncerByName )
137- .flatMap (Optional ::stream )
138- .map (Usersyncer ::getCookieFamilyName )
139- .filter (Predicate .not (StreamUtil .distinctBy (Function .identity ())))
140- .distinct ()
141- .sorted ()
142- .toList ();
143-
144- if (!duplicatedCookieFamilyNames .isEmpty ()) {
136+ .map (Optional ::orElseThrow )
137+ .findAny ().orElseThrow ();
138+
139+ return UsersyncConfig .of (biddersWithoutAliases , preferredUserSyncType (usersyncer ));
140+ }
141+
142+ private static void validateBiddersHaveTheSameVendorId (Set <String > bidders , BidderCatalog bidderCatalog ) {
143+ final Set <Integer > vendorIds = bidders .stream ()
144+ .map (bidderCatalog ::vendorIdByName )
145+ .collect (Collectors .toSet ());
146+
147+ if (vendorIds .size () > 1 ) {
145148 throw new IllegalArgumentException (
146- "Duplicated \" cookie- family- name\" found, values: "
147- + String . join ( ", " , duplicatedCookieFamilyNames ));
149+ "Found bidders with the same cookie family name but different vendor ids. "
150+ + "Bidders: %s. Vendor ids: %s" . formatted ( bidders , vendorIds ));
148151 }
149152 }
150153
151- private static boolean isAliasWithRootCookieFamilyName (BidderCatalog bidderCatalog , String bidder ) {
152- final String bidderCookieFamilyName = bidderCatalog .cookieFamilyName (bidder ).orElse (StringUtils .EMPTY );
153- final String parentCookieFamilyName =
154- bidderCatalog .cookieFamilyName (bidderCatalog .resolveBaseBidder (bidder )).orElse (null );
154+ private static void validateBiddersHaveTheSameUsersyncConfig (Set <String > bidders , BidderCatalog bidderCatalog ) {
155+ final Set <Usersyncer > usersyncers = bidders .stream ()
156+ .map (bidderCatalog ::usersyncerByName )
157+ .map (Optional ::orElseThrow )
158+ .collect (Collectors .toSet ());
155159
156- return bidderCatalog .isAlias (bidder )
157- && parentCookieFamilyName != null
158- && parentCookieFamilyName .equals (bidderCookieFamilyName );
160+ if (usersyncers .size () > 1 ) {
161+ throw new IllegalArgumentException (
162+ "Found bidders with the same cookie family name but different usersync configs. "
163+ + "Bidders: %s. Usersync configs: %s" .formatted (bidders , usersyncers ));
164+ }
159165 }
160166
161167 private static UsersyncMethodType preferredUserSyncType (Usersyncer usersyncer ) {
@@ -181,8 +187,8 @@ private Future<SetuidContext> toSetuidContext(RoutingContext routingContext) {
181187 final Timeout timeout = timeoutFactory .create (defaultTimeout );
182188
183189 final UsersyncMethodType syncType = Optional .ofNullable (cookieName )
184- .map (cookieNameToBidderAndSyncType ::get )
185- .map (Pair :: getRight )
190+ .map (cookieFamilyNameToUsersyncConfig ::get )
191+ .map (UsersyncConfig :: getUsersyncMethodType )
186192 .orElse (null );
187193
188194 return accountById (requestAccount , timeout )
@@ -236,15 +242,14 @@ private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextR
236242 final AccountPrivacyConfig privacyConfig = setuidContext .getAccount ().getPrivacy ();
237243 final AccountGdprConfig accountGdprConfig = privacyConfig != null ? privacyConfig .getGdpr () : null ;
238244
239- final String bidderName = cookieNameToBidderAndSyncType .get (bidderCookieFamily ).getLeft ();
245+ final Set < String > bidderNames = cookieFamilyNameToUsersyncConfig .get (bidderCookieFamily ).getBidders ();
240246
241247 Future .all (
242248 tcfDefinerService .isAllowedForHostVendorId (tcfContext ),
243- tcfDefinerService .resultForBidderNames (
244- Collections .singleton (bidderName ), tcfContext , accountGdprConfig ))
249+ tcfDefinerService .resultForBidderNames (bidderNames , tcfContext , accountGdprConfig ))
245250 .onComplete (hostTcfResponseResult -> respondByTcfResponse (
246251 hostTcfResponseResult ,
247- bidderName ,
252+ bidderNames ,
248253 setuidContext ));
249254 } else {
250255 final Throwable error = setuidContextResult .cause ();
@@ -255,7 +260,7 @@ private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextR
255260 private void validateSetuidContext (SetuidContext setuidContext , String bidderCookieFamily ) {
256261 final String cookieName = setuidContext .getCookieName ();
257262 final boolean isCookieNameBlank = StringUtils .isBlank (cookieName );
258- if (isCookieNameBlank || !cookieNameToBidderAndSyncType .containsKey (cookieName )) {
263+ if (isCookieNameBlank || !cookieFamilyNameToUsersyncConfig .containsKey (cookieName )) {
259264 final String cookieNameError = isCookieNameBlank ? "required" : "invalid" ;
260265 throw new InvalidRequestException ("\" bidder\" query param is " + cookieNameError );
261266 }
@@ -282,7 +287,7 @@ private void validateSetuidContext(SetuidContext setuidContext, String bidderCoo
282287 }
283288
284289 private void respondByTcfResponse (AsyncResult <CompositeFuture > hostTcfResponseResult ,
285- String bidderName ,
290+ Set < String > bidderNames ,
286291 SetuidContext setuidContext ) {
287292
288293 final TcfContext tcfContext = setuidContext .getPrivacyContext ().getTcfContext ();
@@ -293,9 +298,11 @@ private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseRe
293298 final HostVendorTcfResponse hostVendorTcfResponse = compositeFuture .resultAt (0 );
294299 final TcfResponse <String > bidderTcfResponse = compositeFuture .resultAt (1 );
295300
301+ // bidders corresponding to the same cookie family name should have the same vendor id
302+ // so we can use any one of them here
296303 final Map <String , PrivacyEnforcementAction > vendorIdToAction = bidderTcfResponse .getActions ();
297304 final PrivacyEnforcementAction action = vendorIdToAction != null
298- ? vendorIdToAction .get (bidderName )
305+ ? vendorIdToAction .get (bidderNames . iterator (). next () )
299306 : null ;
300307
301308 final boolean notInGdprScope = BooleanUtils .isFalse (bidderTcfResponse .getUserInGdprScope ());
@@ -416,4 +423,12 @@ private void handleErrors(Throwable error, RoutingContext routingContext, TcfCon
416423 private void addCookie (RoutingContext routingContext , Cookie cookie ) {
417424 routingContext .response ().headers ().add (HttpUtil .SET_COOKIE_HEADER , cookie .encode ());
418425 }
426+
427+ @ Value (staticConstructor = "of" )
428+ private static class UsersyncConfig {
429+
430+ Set <String > bidders ;
431+
432+ UsersyncMethodType usersyncMethodType ;
433+ }
419434}
0 commit comments