1010import io .vertx .core .http .HttpServerRequest ;
1111import io .vertx .core .http .HttpServerResponse ;
1212import io .vertx .ext .web .RoutingContext ;
13- import org .apache .commons .collections4 .CollectionUtils ;
1413import org .apache .commons .lang3 .BooleanUtils ;
14+ import org .apache .commons .lang3 .ObjectUtils ;
1515import org .apache .commons .lang3 .StringUtils ;
16+ import org .apache .commons .lang3 .tuple .Pair ;
1617import org .prebid .server .activity .Activity ;
1718import org .prebid .server .activity .ComponentType ;
1819import org .prebid .server .activity .infrastructure .ActivityInfrastructure ;
2728import org .prebid .server .auction .privacy .contextfactory .SetuidPrivacyContextFactory ;
2829import org .prebid .server .bidder .BidderCatalog ;
2930import org .prebid .server .bidder .UsersyncFormat ;
30- import org .prebid .server .bidder .UsersyncMethod ;
3131import org .prebid .server .bidder .UsersyncMethodType ;
3232import org .prebid .server .bidder .UsersyncUtil ;
3333import org .prebid .server .bidder .Usersyncer ;
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 ;
5758import org .prebid .server .vertx .verticles .server .HttpEndpoint ;
5859import org .prebid .server .vertx .verticles .server .application .ApplicationResource ;
5960
6061import java .util .Collections ;
62+ import java .util .Comparator ;
6163import java .util .List ;
6264import java .util .Map ;
6365import java .util .Objects ;
6466import java .util .Optional ;
6567import java .util .function .Consumer ;
6668import java .util .function .Function ;
67- import java .util .function .Supplier ;
69+ import java .util .function .Predicate ;
6870import java .util .stream .Collectors ;
69- import java .util .stream .Stream ;
7071
7172public class SetuidHandler implements ApplicationResource {
7273
@@ -88,7 +89,7 @@ public class SetuidHandler implements ApplicationResource {
8889 private final AnalyticsReporterDelegator analyticsDelegator ;
8990 private final Metrics metrics ;
9091 private final TimeoutFactory timeoutFactory ;
91- private final Map <String , UsersyncMethodType > cookieNameToSyncType ;
92+ private final Map <String , Pair < String , UsersyncMethodType >> cookieNameToBidderAndSyncType ;
9293
9394 public SetuidHandler (long defaultTimeout ,
9495 UidsCookieService uidsCookieService ,
@@ -112,52 +113,57 @@ public SetuidHandler(long defaultTimeout,
112113 this .analyticsDelegator = Objects .requireNonNull (analyticsDelegator );
113114 this .metrics = Objects .requireNonNull (metrics );
114115 this .timeoutFactory = Objects .requireNonNull (timeoutFactory );
115- this .cookieNameToSyncType = collectMap (bidderCatalog );
116+ this .cookieNameToBidderAndSyncType = collectUsersyncers (bidderCatalog );
116117 }
117118
118- private static Map <String , UsersyncMethodType > collectMap (BidderCatalog bidderCatalog ) {
119+ private static Map <String , Pair <String , UsersyncMethodType >> collectUsersyncers (BidderCatalog bidderCatalog ) {
120+ validateUsersyncersDuplicates (bidderCatalog );
121+
122+ 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 ()))));
131+ }
119132
120- final Supplier < Stream < Usersyncer >> usersyncers = () -> bidderCatalog . names ()
121- .stream ()
122- .filter (bidderCatalog :: isActive )
133+ private static void validateUsersyncersDuplicates ( BidderCatalog bidderCatalog ) {
134+ final List < String > duplicatedCookieFamilyNames = bidderCatalog . usersyncReadyBidders () .stream ()
135+ .filter (bidderName -> ! isAliasWithRootCookieFamilyName ( bidderCatalog , bidderName ) )
123136 .map (bidderCatalog ::usersyncerByName )
124- .filter (Optional ::isPresent )
125- .map (Optional :: get )
126- .distinct ();
127-
128- validateUsersyncers ( usersyncers . get () );
137+ .flatMap (Optional ::stream )
138+ .map (Usersyncer :: getCookieFamilyName )
139+ .filter ( Predicate . not ( StreamUtil . distinctBy ( Function . identity ())))
140+ . distinct ()
141+ . toList ( );
129142
130- return usersyncers .get ()
131- .collect (Collectors .toMap (Usersyncer ::getCookieFamilyName , SetuidHandler ::preferredUserSyncType ));
143+ if (!duplicatedCookieFamilyNames .isEmpty ()) {
144+ throw new IllegalArgumentException (
145+ "Duplicated \" cookie-family-name\" found, values: "
146+ + String .join (", " , duplicatedCookieFamilyNames ));
147+ }
132148 }
133149
134- @ Override
135- public List <HttpEndpoint > endpoints () {
136- return Collections .singletonList (HttpEndpoint .of (HttpMethod .GET , Endpoint .setuid .value ()));
150+ private static boolean isAliasWithRootCookieFamilyName (BidderCatalog bidderCatalog , String bidder ) {
151+ final String bidderCookieFamilyName = bidderCatalog .cookieFamilyName (bidder ).orElse (StringUtils .EMPTY );
152+ final String parentCookieFamilyName =
153+ bidderCatalog .cookieFamilyName (bidderCatalog .resolveBaseBidder (bidder )).orElse (null );
154+
155+ return bidderCatalog .isAlias (bidder )
156+ && parentCookieFamilyName != null
157+ && parentCookieFamilyName .equals (bidderCookieFamilyName );
137158 }
138159
139160 private static UsersyncMethodType preferredUserSyncType (Usersyncer usersyncer ) {
140- return Stream .of (usersyncer .getIframe (), usersyncer .getRedirect ())
141- .filter (Objects ::nonNull )
142- .findFirst ()
143- .map (UsersyncMethod ::getType )
144- .get (); // when usersyncer is present, it will contain at least one method
161+ return ObjectUtils .firstNonNull (usersyncer .getIframe (), usersyncer .getRedirect ()).getType ();
145162 }
146163
147- private static void validateUsersyncers (Stream <Usersyncer > usersyncers ) {
148- final List <String > cookieFamilyNameDuplicates = usersyncers .map (Usersyncer ::getCookieFamilyName )
149- .collect (Collectors .groupingBy (Function .identity (), Collectors .counting ()))
150- .entrySet ()
151- .stream ()
152- .filter (name -> name .getValue () > 1 )
153- .map (Map .Entry ::getKey )
154- .distinct ()
155- .toList ();
156- if (CollectionUtils .isNotEmpty (cookieFamilyNameDuplicates )) {
157- throw new IllegalArgumentException (
158- "Duplicated \" cookie-family-name\" found, values: "
159- + String .join (", " , cookieFamilyNameDuplicates ));
160- }
164+ @ Override
165+ public List <HttpEndpoint > endpoints () {
166+ return Collections .singletonList (HttpEndpoint .of (HttpMethod .GET , Endpoint .setuid .value ()));
161167 }
162168
163169 @ Override
@@ -173,6 +179,11 @@ private Future<SetuidContext> toSetuidContext(RoutingContext routingContext) {
173179 final String requestAccount = httpRequest .getParam (ACCOUNT_PARAM );
174180 final Timeout timeout = timeoutFactory .create (defaultTimeout );
175181
182+ final UsersyncMethodType syncType = Optional .ofNullable (cookieName )
183+ .map (cookieNameToBidderAndSyncType ::get )
184+ .map (Pair ::getRight )
185+ .orElse (null );
186+
176187 return accountById (requestAccount , timeout )
177188 .compose (account -> setuidPrivacyContextFactory .contextFrom (httpRequest , account , timeout )
178189 .map (privacyContext -> SetuidContext .builder ()
@@ -181,7 +192,7 @@ private Future<SetuidContext> toSetuidContext(RoutingContext routingContext) {
181192 .timeout (timeout )
182193 .account (account )
183194 .cookieName (cookieName )
184- .syncType (cookieNameToSyncType . get ( cookieName ) )
195+ .syncType (syncType )
185196 .privacyContext (privacyContext )
186197 .build ()))
187198
@@ -211,11 +222,11 @@ private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextR
211222
212223 if (setuidContextResult .succeeded ()) {
213224 final SetuidContext setuidContext = setuidContextResult .result ();
214- final String bidderCookieName = setuidContext .getCookieName ();
225+ final String bidderCookieFamily = setuidContext .getCookieName ();
215226 final TcfContext tcfContext = setuidContext .getPrivacyContext ().getTcfContext ();
216227
217228 try {
218- validateSetuidContext (setuidContext , bidderCookieName );
229+ validateSetuidContext (setuidContext , bidderCookieFamily );
219230 } catch (InvalidRequestException | UnauthorizedUidsException | UnavailableForLegalReasonsException e ) {
220231 handleErrors (e , routingContext , tcfContext );
221232 return ;
@@ -224,28 +235,33 @@ private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextR
224235 final AccountPrivacyConfig privacyConfig = setuidContext .getAccount ().getPrivacy ();
225236 final AccountGdprConfig accountGdprConfig = privacyConfig != null ? privacyConfig .getGdpr () : null ;
226237
238+ final String bidderName = cookieNameToBidderAndSyncType .get (bidderCookieFamily ).getLeft ();
239+
227240 Future .all (
228- tcfDefinerService .isAllowedForHostVendorId (tcfContext ),
229- tcfDefinerService .resultForBidderNames (
230- Collections .singleton (bidderCookieName ), tcfContext , accountGdprConfig ))
231- .onComplete (hostTcfResponseResult -> respondByTcfResponse (hostTcfResponseResult , setuidContext ));
241+ tcfDefinerService .isAllowedForHostVendorId (tcfContext ),
242+ tcfDefinerService .resultForBidderNames (
243+ Collections .singleton (bidderName ), tcfContext , accountGdprConfig ))
244+ .onComplete (hostTcfResponseResult -> respondByTcfResponse (
245+ hostTcfResponseResult ,
246+ bidderName ,
247+ setuidContext ));
232248 } else {
233249 final Throwable error = setuidContextResult .cause ();
234250 handleErrors (error , routingContext , null );
235251 }
236252 }
237253
238- private void validateSetuidContext (SetuidContext setuidContext , String bidder ) {
254+ private void validateSetuidContext (SetuidContext setuidContext , String bidderCookieFamily ) {
239255 final String cookieName = setuidContext .getCookieName ();
240256 final boolean isCookieNameBlank = StringUtils .isBlank (cookieName );
241- if (isCookieNameBlank || !cookieNameToSyncType .containsKey (cookieName )) {
257+ if (isCookieNameBlank || !cookieNameToBidderAndSyncType .containsKey (cookieName )) {
242258 final String cookieNameError = isCookieNameBlank ? "required" : "invalid" ;
243259 throw new InvalidRequestException ("\" bidder\" query param is " + cookieNameError );
244260 }
245261
246262 final TcfContext tcfContext = setuidContext .getPrivacyContext ().getTcfContext ();
247263 if (tcfContext .isInGdprScope () && !tcfContext .isConsentValid ()) {
248- metrics .updateUserSyncTcfInvalidMetric (bidder );
264+ metrics .updateUserSyncTcfInvalidMetric (bidderCookieFamily );
249265 throw new InvalidRequestException ("Consent string is invalid" );
250266 }
251267
@@ -256,16 +272,18 @@ private void validateSetuidContext(SetuidContext setuidContext, String bidder) {
256272
257273 final ActivityInfrastructure activityInfrastructure = setuidContext .getActivityInfrastructure ();
258274 final ActivityInvocationPayload activityInvocationPayload = TcfContextActivityInvocationPayload .of (
259- ActivityInvocationPayloadImpl .of (ComponentType .BIDDER , bidder ),
275+ ActivityInvocationPayloadImpl .of (ComponentType .BIDDER , bidderCookieFamily ),
260276 tcfContext );
261277
262278 if (!activityInfrastructure .isAllowed (Activity .SYNC_USER , activityInvocationPayload )) {
263279 throw new UnavailableForLegalReasonsException ();
264280 }
265281 }
266282
267- private void respondByTcfResponse (AsyncResult <CompositeFuture > hostTcfResponseResult , SetuidContext setuidContext ) {
268- final String bidderCookieName = setuidContext .getCookieName ();
283+ private void respondByTcfResponse (AsyncResult <CompositeFuture > hostTcfResponseResult ,
284+ String bidderName ,
285+ SetuidContext setuidContext ) {
286+
269287 final TcfContext tcfContext = setuidContext .getPrivacyContext ().getTcfContext ();
270288 final RoutingContext routingContext = setuidContext .getRoutingContext ();
271289
@@ -276,7 +294,7 @@ private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseRe
276294
277295 final Map <String , PrivacyEnforcementAction > vendorIdToAction = bidderTcfResponse .getActions ();
278296 final PrivacyEnforcementAction action = vendorIdToAction != null
279- ? vendorIdToAction .get (bidderCookieName )
297+ ? vendorIdToAction .get (bidderName )
280298 : null ;
281299
282300 final boolean notInGdprScope = BooleanUtils .isFalse (bidderTcfResponse .getUserInGdprScope ());
@@ -285,7 +303,7 @@ private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseRe
285303 if (hostVendorTcfResponse .isVendorAllowed () && isBidderVendorAllowed ) {
286304 respondWithCookie (setuidContext );
287305 } else {
288- metrics .updateUserSyncTcfBlockedMetric (bidderCookieName );
306+ metrics .updateUserSyncTcfBlockedMetric (setuidContext . getCookieName () );
289307
290308 final HttpResponseStatus status = new HttpResponseStatus (UNAVAILABLE_FOR_LEGAL_REASONS ,
291309 "Unavailable for legal reasons" );
@@ -300,7 +318,7 @@ private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseRe
300318 }
301319 } else {
302320 final Throwable error = hostTcfResponseResult .cause ();
303- metrics .updateUserSyncTcfBlockedMetric (bidderCookieName );
321+ metrics .updateUserSyncTcfBlockedMetric (setuidContext . getCookieName () );
304322 handleErrors (error , routingContext , tcfContext );
305323 }
306324 }
0 commit comments