View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.common.lib;
20  
21  import java.util.Collection;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.stream.Collectors;
28  import org.apache.commons.lang3.SerializationUtils;
29  import org.apache.commons.lang3.StringUtils;
30  import org.apache.commons.lang3.tuple.Pair;
31  import org.apache.syncope.common.lib.request.AbstractReplacePatchItem;
32  import org.apache.syncope.common.lib.request.AnyObjectUR;
33  import org.apache.syncope.common.lib.request.AnyUR;
34  import org.apache.syncope.common.lib.request.AttrPatch;
35  import org.apache.syncope.common.lib.request.BooleanReplacePatchItem;
36  import org.apache.syncope.common.lib.request.GroupUR;
37  import org.apache.syncope.common.lib.request.LinkedAccountUR;
38  import org.apache.syncope.common.lib.request.MembershipUR;
39  import org.apache.syncope.common.lib.request.PasswordPatch;
40  import org.apache.syncope.common.lib.request.RelationshipUR;
41  import org.apache.syncope.common.lib.request.StringPatchItem;
42  import org.apache.syncope.common.lib.request.StringReplacePatchItem;
43  import org.apache.syncope.common.lib.request.UserUR;
44  import org.apache.syncope.common.lib.to.AnyObjectTO;
45  import org.apache.syncope.common.lib.to.AnyTO;
46  import org.apache.syncope.common.lib.to.GroupTO;
47  import org.apache.syncope.common.lib.to.LinkedAccountTO;
48  import org.apache.syncope.common.lib.to.MembershipTO;
49  import org.apache.syncope.common.lib.to.RelationshipTO;
50  import org.apache.syncope.common.lib.to.UserTO;
51  import org.apache.syncope.common.lib.types.PatchOperation;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  /**
56   * Utility class for comparing {@link AnyTO} instances in order to generate {@link AnyUR} instances.
57   */
58  public final class AnyOperations {
59  
60      private static final Logger LOG = LoggerFactory.getLogger(AnyOperations.class);
61  
62      private static final List<String> NULL_SINGLETON_LIST = Collections.singletonList(null);
63  
64      private AnyOperations() {
65          // empty constructor for static utility classes
66      }
67  
68      private static <T, K extends AbstractReplacePatchItem<T>> K replacePatchItem(
69              final T updated, final T original, final K proto) {
70  
71          if ((original == null && updated == null) || (original != null && original.equals(updated))) {
72              return null;
73          }
74  
75          proto.setValue(updated);
76          return proto;
77      }
78  
79      private static void diff(
80              final AnyTO updated, final AnyTO original, final AnyUR result, final boolean incremental) {
81  
82          // check same key
83          if (updated.getKey() == null && original.getKey() != null
84                  || (updated.getKey() != null && !updated.getKey().equals(original.getKey()))) {
85  
86              throw new IllegalArgumentException("AnyTO's key must be the same");
87          }
88          result.setKey(updated.getKey());
89  
90          // 1. realm
91          result.setRealm(replacePatchItem(updated.getRealm(), original.getRealm(), new StringReplacePatchItem()));
92  
93          // 2. auxilairy classes
94          result.getAuxClasses().clear();
95  
96          if (!incremental) {
97              original.getAuxClasses().stream().filter(auxClass -> !updated.getAuxClasses().contains(auxClass)).
98                      forEach(auxClass -> result.getAuxClasses().add(new StringPatchItem.Builder().
99                      operation(PatchOperation.DELETE).value(auxClass).build()));
100         }
101 
102         updated.getAuxClasses().stream().filter(auxClass -> !original.getAuxClasses().contains(auxClass)).
103                 forEach(auxClass -> result.getAuxClasses().add(new StringPatchItem.Builder().
104                 operation(PatchOperation.ADD_REPLACE).value(auxClass).build()));
105 
106         // 3. plain attributes
107         Map<String, Attr> updatedAttrs = EntityTOUtils.buildAttrMap(updated.getPlainAttrs());
108         Map<String, Attr> originalAttrs = EntityTOUtils.buildAttrMap(original.getPlainAttrs());
109 
110         result.getPlainAttrs().clear();
111 
112         if (!incremental) {
113             originalAttrs.keySet().stream().
114                     filter(attr -> !updatedAttrs.containsKey(attr)).forEach(
115                     schema -> result.getPlainAttrs().add(
116                             new AttrPatch.Builder(new Attr.Builder(schema).build()).
117                                     operation(PatchOperation.DELETE).
118                                     build()));
119         }
120 
121         updatedAttrs.values().forEach(attr -> {
122             if (isEmpty(attr)) {
123                 if (!incremental) {
124                     result.getPlainAttrs().add(
125                             new AttrPatch.Builder(new Attr.Builder(attr.getSchema()).build()).
126                                     operation(PatchOperation.DELETE).
127                                     build());
128                 }
129             } else if (!originalAttrs.containsKey(attr.getSchema())
130                     || !originalAttrs.get(attr.getSchema()).getValues().equals(attr.getValues())) {
131 
132                 AttrPatch patch = new AttrPatch.Builder(attr).operation(PatchOperation.ADD_REPLACE).build();
133                 if (!patch.isEmpty()) {
134                     result.getPlainAttrs().add(patch);
135                 }
136             }
137         });
138 
139         // 4. virtual attributes
140         result.getVirAttrs().clear();
141         result.getVirAttrs().addAll(updated.getVirAttrs());
142 
143         // 5. resources
144         result.getResources().clear();
145 
146         if (!incremental) {
147             original.getResources().stream().filter(resource -> !updated.getResources().contains(resource)).
148                     forEach(resource -> result.getResources().add(new StringPatchItem.Builder().
149                     operation(PatchOperation.DELETE).value(resource).build()));
150         }
151 
152         updated.getResources().stream().filter(resource -> !original.getResources().contains(resource)).
153                 forEach(resource -> result.getResources().add(new StringPatchItem.Builder().
154                 operation(PatchOperation.ADD_REPLACE).value(resource).build()));
155     }
156 
157     /**
158      * Calculate modifications needed by first in order to be equal to second.
159      *
160      * @param updated updated AnyObjectTO
161      * @param original original AnyObjectTO
162      * @param incremental perform incremental diff (without removing existing info)
163      * @return {@link AnyObjectUR} containing differences
164      */
165     public static AnyObjectUR diff(
166             final AnyObjectTO updated, final AnyObjectTO original, final boolean incremental) {
167 
168         AnyObjectUR result = new AnyObjectUR();
169 
170         diff(updated, original, result, incremental);
171 
172         // 1. name
173         result.setName(replacePatchItem(updated.getName(), original.getName(), new StringReplacePatchItem()));
174 
175         // 2. relationships
176         Map<Pair<String, String>, RelationshipTO> updatedRels =
177                 EntityTOUtils.buildRelationshipMap(updated.getRelationships());
178         Map<Pair<String, String>, RelationshipTO> originalRels =
179                 EntityTOUtils.buildRelationshipMap(original.getRelationships());
180 
181         updatedRels.entrySet().stream().
182                 filter(entry -> (!originalRels.containsKey(entry.getKey()))).
183                 forEach(entry -> result.getRelationships().add(new RelationshipUR.Builder(entry.getValue()).
184                 operation(PatchOperation.ADD_REPLACE).build()));
185 
186         if (!incremental) {
187             originalRels.keySet().stream().filter(relationship -> !updatedRels.containsKey(relationship)).
188                     forEach(key -> result.getRelationships().add(new RelationshipUR.Builder(originalRels.get(key)).
189                     operation(PatchOperation.DELETE).build()));
190         }
191 
192         // 3. memberships
193         Map<String, MembershipTO> updatedMembs = EntityTOUtils.buildMembershipMap(updated.getMemberships());
194         Map<String, MembershipTO> originalMembs = EntityTOUtils.buildMembershipMap(original.getMemberships());
195 
196         updatedMembs.forEach((key, value) -> {
197             MembershipUR membershipPatch = new MembershipUR.Builder(value.getGroupKey()).
198                     operation(PatchOperation.ADD_REPLACE).build();
199 
200             diff(value, membershipPatch);
201 
202             if (!originalMembs.containsKey(key)
203                     || (!membershipPatch.getPlainAttrs().isEmpty() || !membershipPatch.getVirAttrs().isEmpty())) {
204 
205                 result.getMemberships().add(membershipPatch);
206             }
207         });
208 
209         if (!incremental) {
210             originalMembs.keySet().stream().filter(membership -> !updatedMembs.containsKey(membership)).
211                     forEach(key -> result.getMemberships().add(
212                     new MembershipUR.Builder(originalMembs.get(key).getGroupKey()).
213                             operation(PatchOperation.DELETE).build()));
214         }
215 
216         return result;
217     }
218 
219     private static void diff(
220             final MembershipTO updated,
221             final MembershipUR result) {
222 
223         // 1. plain attributes
224         result.getPlainAttrs().addAll(updated.getPlainAttrs().stream().
225                 filter(attr -> !isEmpty(attr)).
226                 collect(Collectors.toSet()));
227 
228         // 2. virtual attributes
229         result.getVirAttrs().clear();
230         result.getVirAttrs().addAll(updated.getVirAttrs());
231     }
232 
233     /**
234      * Calculate modifications needed by first in order to be equal to second.
235      *
236      * @param updated updated UserTO
237      * @param original original UserTO
238      * @param incremental perform incremental diff (without removing existing info)
239      * @return {@link UserUR} containing differences
240      */
241     public static UserUR diff(final UserTO updated, final UserTO original, final boolean incremental) {
242         UserUR result = new UserUR();
243 
244         diff(updated, original, result, incremental);
245 
246         // 1. password
247         if (updated.getPassword() != null
248                 && (original.getPassword() == null || !original.getPassword().equals(updated.getPassword()))) {
249 
250             result.setPassword(new PasswordPatch.Builder().
251                     value(updated.getPassword()).
252                     resources(updated.getResources()).build());
253         }
254 
255         // 2. username
256         result.setUsername(
257                 replacePatchItem(updated.getUsername(), original.getUsername(), new StringReplacePatchItem()));
258 
259         // 3. security question / answer
260         if (updated.getSecurityQuestion() == null) {
261             result.setSecurityQuestion(null);
262             result.setSecurityAnswer(null);
263         } else if (!updated.getSecurityQuestion().equals(original.getSecurityQuestion())
264                 || StringUtils.isNotBlank(updated.getSecurityAnswer())) {
265 
266             result.setSecurityQuestion(new StringReplacePatchItem.Builder().
267                     value(updated.getSecurityQuestion()).build());
268             result.setSecurityAnswer(
269                     new StringReplacePatchItem.Builder().value(updated.getSecurityAnswer()).build());
270         }
271 
272         result.setMustChangePassword(replacePatchItem(
273                 updated.isMustChangePassword(), original.isMustChangePassword(), new BooleanReplacePatchItem()));
274 
275         // 4. roles
276         if (!incremental) {
277             original.getRoles().stream().filter(role -> !updated.getRoles().contains(role)).
278                     forEach(toRemove -> result.getRoles().add(new StringPatchItem.Builder().
279                     operation(PatchOperation.DELETE).value(toRemove).build()));
280         }
281 
282         updated.getRoles().stream().filter(role -> !original.getRoles().contains(role)).
283                 forEach(toAdd -> result.getRoles().add(new StringPatchItem.Builder().
284                 operation(PatchOperation.ADD_REPLACE).value(toAdd).build()));
285 
286         // 5. relationships
287         Map<Pair<String, String>, RelationshipTO> updatedRels =
288                 EntityTOUtils.buildRelationshipMap(updated.getRelationships());
289         Map<Pair<String, String>, RelationshipTO> originalRels =
290                 EntityTOUtils.buildRelationshipMap(original.getRelationships());
291 
292         updatedRels.entrySet().stream().
293                 filter(entry -> (!originalRels.containsKey(entry.getKey()))).
294                 forEach(entry -> result.getRelationships().add(new RelationshipUR.Builder(entry.getValue()).
295                 operation(PatchOperation.ADD_REPLACE).build()));
296 
297         if (!incremental) {
298             originalRels.keySet().stream().filter(relationship -> !updatedRels.containsKey(relationship)).
299                     forEach(key -> result.getRelationships().add(new RelationshipUR.Builder(originalRels.get(key)).
300                     operation(PatchOperation.DELETE).build()));
301         }
302 
303         // 6. memberships
304         Map<String, MembershipTO> updatedMembs = EntityTOUtils.buildMembershipMap(updated.getMemberships());
305         Map<String, MembershipTO> originalMembs = EntityTOUtils.buildMembershipMap(original.getMemberships());
306 
307         updatedMembs.forEach((key, value) -> {
308             MembershipUR membershipPatch = new MembershipUR.Builder(value.getGroupKey()).
309                     operation(PatchOperation.ADD_REPLACE).build();
310 
311             diff(value, membershipPatch);
312 
313             if (!originalMembs.containsKey(key)
314                     || (!membershipPatch.getPlainAttrs().isEmpty() || !membershipPatch.getVirAttrs().isEmpty())) {
315 
316                 result.getMemberships().add(membershipPatch);
317             }
318         });
319 
320         if (!incremental) {
321             originalMembs.keySet().stream().filter(membership -> !updatedMembs.containsKey(membership))
322                     .forEach(key -> result.getMemberships()
323                     .add(new MembershipUR.Builder(originalMembs.get(key).getGroupKey())
324                             .operation(PatchOperation.DELETE).build()));
325         }
326 
327         // 7. linked accounts
328         Map<Pair<String, String>, LinkedAccountTO> updatedAccounts =
329                 EntityTOUtils.buildLinkedAccountMap(updated.getLinkedAccounts());
330         Map<Pair<String, String>, LinkedAccountTO> originalAccounts =
331                 EntityTOUtils.buildLinkedAccountMap(original.getLinkedAccounts());
332 
333         updatedAccounts.entrySet().stream().
334                 forEachOrdered(entry -> {
335                     result.getLinkedAccounts().add(new LinkedAccountUR.Builder().
336                             operation(PatchOperation.ADD_REPLACE).
337                             linkedAccountTO(entry.getValue()).build());
338                 });
339 
340         if (!incremental) {
341             originalAccounts.keySet().stream().filter(account -> !updatedAccounts.containsKey(account)).
342                     forEach(key -> {
343                         result.getLinkedAccounts().add(new LinkedAccountUR.Builder().
344                                 operation(PatchOperation.DELETE).
345                                 linkedAccountTO(originalAccounts.get(key)).build());
346                     });
347         }
348 
349         return result;
350     }
351 
352     /**
353      * Calculate modifications needed by first in order to be equal to second.
354      *
355      * @param updated updated GroupTO
356      * @param original original GroupTO
357      * @param incremental perform incremental diff (without removing existing info)
358      * @return {@link GroupUR} containing differences
359      */
360     public static GroupUR diff(final GroupTO updated, final GroupTO original, final boolean incremental) {
361         GroupUR result = new GroupUR();
362 
363         diff(updated, original, result, incremental);
364 
365         // 1. name
366         result.setName(replacePatchItem(updated.getName(), original.getName(), new StringReplacePatchItem()));
367 
368         // 2. ownership
369         result.setUserOwner(
370                 replacePatchItem(updated.getUserOwner(), original.getUserOwner(), new StringReplacePatchItem()));
371         result.setGroupOwner(
372                 replacePatchItem(updated.getGroupOwner(), original.getGroupOwner(), new StringReplacePatchItem()));
373 
374         // 3. dynamic membership
375         result.setUDynMembershipCond(updated.getUDynMembershipCond());
376         result.getADynMembershipConds().putAll(updated.getADynMembershipConds());
377 
378         // 4. type extensions
379         result.getTypeExtensions().addAll(updated.getTypeExtensions());
380 
381         return result;
382     }
383 
384     @SuppressWarnings("unchecked")
385     public static <TO extends AnyTO, P extends AnyUR> P diff(
386             final TO updated, final TO original, final boolean incremental) {
387 
388         if (updated instanceof UserTO && original instanceof UserTO) {
389             return (P) diff((UserTO) updated, (UserTO) original, incremental);
390         } else if (updated instanceof GroupTO && original instanceof GroupTO) {
391             return (P) diff((GroupTO) updated, (GroupTO) original, incremental);
392         } else if (updated instanceof AnyObjectTO && original instanceof AnyObjectTO) {
393             return (P) diff((AnyObjectTO) updated, (AnyObjectTO) original, incremental);
394         }
395 
396         throw new IllegalArgumentException("Unsupported: " + updated.getClass().getName());
397     }
398 
399     private static Collection<Attr> patch(final Map<String, Attr> attrs, final Set<AttrPatch> attrPatches) {
400         Map<String, Attr> rwattrs = new HashMap<>(attrs);
401         attrPatches.forEach(patch -> {
402             if (patch.getAttr() == null) {
403                 LOG.warn("Invalid {} specified: {}", AttrPatch.class.getName(), patch);
404             } else {
405                 rwattrs.remove(patch.getAttr().getSchema());
406                 if (patch.getOperation() == PatchOperation.ADD_REPLACE && !patch.getAttr().getValues().isEmpty()) {
407                     rwattrs.put(patch.getAttr().getSchema(), patch.getAttr());
408                 }
409             }
410         });
411 
412         return rwattrs.values();
413     }
414 
415     private static <T extends AnyTO, K extends AnyUR> void patch(final T to, final K req, final T result) {
416         // check same key
417         if (to.getKey() == null || !to.getKey().equals(req.getKey())) {
418             throw new IllegalArgumentException(
419                     to.getClass().getSimpleName() + " and "
420                     + req.getClass().getSimpleName() + " keys must be the same");
421         }
422 
423         // 0. realm
424         if (req.getRealm() != null) {
425             result.setRealm(req.getRealm().getValue());
426         }
427 
428         // 1. auxiliary classes
429         for (StringPatchItem auxClassPatch : req.getAuxClasses()) {
430             switch (auxClassPatch.getOperation()) {
431                 case ADD_REPLACE:
432                     result.getAuxClasses().add(auxClassPatch.getValue());
433                     break;
434 
435                 case DELETE:
436                 default:
437                     result.getAuxClasses().remove(auxClassPatch.getValue());
438             }
439         }
440 
441         // 2. plain attributes
442         result.getPlainAttrs().clear();
443         result.getPlainAttrs().addAll(patch(EntityTOUtils.buildAttrMap(to.getPlainAttrs()), req.getPlainAttrs()));
444 
445         // 3. virtual attributes
446         result.getVirAttrs().clear();
447         result.getVirAttrs().addAll(req.getVirAttrs());
448 
449         // 4. resources
450         for (StringPatchItem resourcePatch : req.getResources()) {
451             switch (resourcePatch.getOperation()) {
452                 case ADD_REPLACE:
453                     result.getResources().add(resourcePatch.getValue());
454                     break;
455 
456                 case DELETE:
457                 default:
458                     result.getResources().remove(resourcePatch.getValue());
459             }
460         }
461     }
462 
463     public static AnyTO patch(final AnyTO anyTO, final AnyUR anyUR) {
464         if (anyTO instanceof UserTO) {
465             return patch((UserTO) anyTO, (UserUR) anyUR);
466         }
467         if (anyTO instanceof GroupTO) {
468             return patch((GroupTO) anyTO, (GroupUR) anyUR);
469         }
470         if (anyTO instanceof AnyObjectTO) {
471             return patch((AnyObjectTO) anyTO, (AnyObjectUR) anyUR);
472         }
473         return null;
474     }
475 
476     public static GroupTO patch(final GroupTO groupTO, final GroupUR groupUR) {
477         GroupTO result = SerializationUtils.clone(groupTO);
478         patch(groupTO, groupUR, result);
479 
480         if (groupUR.getName() != null) {
481             result.setName(groupUR.getName().getValue());
482         }
483 
484         if (groupUR.getUserOwner() != null) {
485             result.setGroupOwner(groupUR.getUserOwner().getValue());
486         }
487         if (groupUR.getGroupOwner() != null) {
488             result.setGroupOwner(groupUR.getGroupOwner().getValue());
489         }
490 
491         result.setUDynMembershipCond(groupUR.getUDynMembershipCond());
492         result.getADynMembershipConds().clear();
493         result.getADynMembershipConds().putAll(groupUR.getADynMembershipConds());
494 
495         return result;
496     }
497 
498     public static AnyObjectTO patch(final AnyObjectTO anyObjectTO, final AnyObjectUR anyObjectUR) {
499         AnyObjectTO result = SerializationUtils.clone(anyObjectTO);
500         patch(anyObjectTO, anyObjectUR, result);
501 
502         if (anyObjectUR.getName() != null) {
503             result.setName(anyObjectUR.getName().getValue());
504         }
505 
506         // 1. relationships
507         anyObjectUR.getRelationships().
508                 forEach(relPatch -> {
509                     if (relPatch.getRelationshipTO() == null) {
510                         LOG.warn("Invalid {} specified: {}", RelationshipUR.class.getName(), relPatch);
511                     } else {
512                         result.getRelationships().remove(relPatch.getRelationshipTO());
513                         if (relPatch.getOperation() == PatchOperation.ADD_REPLACE) {
514                             result.getRelationships().add(relPatch.getRelationshipTO());
515                         }
516                     }
517                 });
518 
519         // 2. memberships
520         anyObjectUR.getMemberships().forEach(membPatch -> {
521             if (membPatch.getGroup() == null) {
522                 LOG.warn("Invalid {} specified: {}", MembershipUR.class.getName(), membPatch);
523             } else {
524                 result.getMemberships().stream().
525                         filter(membership -> membPatch.getGroup().equals(membership.getGroupKey())).
526                         findFirst().ifPresent(memb -> result.getMemberships().remove(memb));
527 
528                 if (membPatch.getOperation() == PatchOperation.ADD_REPLACE) {
529                     MembershipTO newMembershipTO = new MembershipTO.Builder(membPatch.getGroup()).
530                             // 3. plain attributes
531                             plainAttrs(membPatch.getPlainAttrs()).
532                             // 4. virtual attributes
533                             virAttrs(membPatch.getVirAttrs()).
534                             build();
535 
536                     result.getMemberships().add(newMembershipTO);
537                 }
538             }
539         });
540 
541         return result;
542     }
543 
544     public static UserTO patch(final UserTO userTO, final UserUR userUR) {
545         UserTO result = SerializationUtils.clone(userTO);
546         patch(userTO, userUR, result);
547 
548         // 1. password
549         if (userUR.getPassword() != null) {
550             result.setPassword(userUR.getPassword().getValue());
551         }
552 
553         // 2. username
554         if (userUR.getUsername() != null) {
555             result.setUsername(userUR.getUsername().getValue());
556         }
557 
558         // 3. relationships
559         userUR.getRelationships().forEach(relPatch -> {
560             if (relPatch.getRelationshipTO() == null) {
561                 LOG.warn("Invalid {} specified: {}", RelationshipUR.class.getName(), relPatch);
562             } else {
563                 result.getRelationships().remove(relPatch.getRelationshipTO());
564                 if (relPatch.getOperation() == PatchOperation.ADD_REPLACE) {
565                     result.getRelationships().add(relPatch.getRelationshipTO());
566                 }
567             }
568         });
569 
570         // 4. memberships
571         userUR.getMemberships().forEach(membPatch -> {
572             if (membPatch.getGroup() == null) {
573                 LOG.warn("Invalid {} specified: {}", MembershipUR.class.getName(), membPatch);
574             } else {
575                 result.getMemberships().stream().
576                         filter(membership -> membPatch.getGroup().equals(membership.getGroupKey())).
577                         findFirst().ifPresent(memb -> result.getMemberships().remove(memb));
578 
579                 if (membPatch.getOperation() == PatchOperation.ADD_REPLACE) {
580                     MembershipTO newMembershipTO = new MembershipTO.Builder(membPatch.getGroup()).
581                             // 3. plain attributes
582                             plainAttrs(membPatch.getPlainAttrs()).
583                             // 4. virtual attributes
584                             virAttrs(membPatch.getVirAttrs()).
585                             build();
586 
587                     result.getMemberships().add(newMembershipTO);
588                 }
589             }
590         });
591 
592         // 5. roles
593         for (StringPatchItem rolePatch : userUR.getRoles()) {
594             switch (rolePatch.getOperation()) {
595                 case ADD_REPLACE:
596                     result.getRoles().add(rolePatch.getValue());
597                     break;
598 
599                 case DELETE:
600                 default:
601                     result.getRoles().remove(rolePatch.getValue());
602             }
603         }
604 
605         // 6. linked accounts
606         userUR.getLinkedAccounts().forEach(accountPatch -> {
607             if (accountPatch.getLinkedAccountTO() == null) {
608                 LOG.warn("Invalid {} specified: {}", LinkedAccountUR.class.getName(), accountPatch);
609             } else {
610                 result.getLinkedAccounts().remove(accountPatch.getLinkedAccountTO());
611                 if (accountPatch.getOperation() == PatchOperation.ADD_REPLACE) {
612                     result.getLinkedAccounts().add(accountPatch.getLinkedAccountTO());
613                 }
614             }
615         });
616 
617         return result;
618     }
619 
620     /**
621      * Add PLAIN attribute DELETE patch for those attributes of the input AnyTO without values or containing null value
622      *
623      * @param anyTO User, Group or Any Object to look for attributes with no value
624      * @param anyUR update req to enrich with DELETE statements
625      */
626     public static void cleanEmptyAttrs(final AnyTO anyTO, final AnyUR anyUR) {
627         anyUR.getPlainAttrs().addAll(anyTO.getPlainAttrs().stream().
628                 filter(AnyOperations::isEmpty).
629                 map(plainAttr -> new AttrPatch.Builder(new Attr.Builder(plainAttr.getSchema()).build()).
630                 operation(PatchOperation.DELETE).
631                 build()).collect(Collectors.toSet()));
632     }
633 
634     private static boolean isEmpty(final Attr attr) {
635         return attr.getValues().isEmpty() || NULL_SINGLETON_LIST.equals(attr.getValues());
636     }
637 }