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             result.getMemberships().add(membershipPatch);
202         });
203 
204         if (!incremental) {
205             originalMembs.keySet().stream().filter(membership -> !updatedMembs.containsKey(membership)).
206                     forEach(key -> result.getMemberships().add(
207                     new MembershipUR.Builder(originalMembs.get(key).getGroupKey()).
208                             operation(PatchOperation.DELETE).build()));
209         }
210 
211         return result;
212     }
213 
214     private static void diff(
215             final MembershipTO updated,
216             final MembershipUR result) {
217 
218         // 1. plain attributes
219         result.getPlainAttrs().addAll(updated.getPlainAttrs().stream().
220                 filter(attr -> !isEmpty(attr)).
221                 collect(Collectors.toSet()));
222 
223         // 2. virtual attributes
224         result.getVirAttrs().clear();
225         result.getVirAttrs().addAll(updated.getVirAttrs());
226     }
227 
228     /**
229      * Calculate modifications needed by first in order to be equal to second.
230      *
231      * @param updated updated UserTO
232      * @param original original UserTO
233      * @param incremental perform incremental diff (without removing existing info)
234      * @return {@link UserUR} containing differences
235      */
236     public static UserUR diff(final UserTO updated, final UserTO original, final boolean incremental) {
237         UserUR result = new UserUR();
238 
239         diff(updated, original, result, incremental);
240 
241         // 1. password
242         if (updated.getPassword() != null
243                 && (original.getPassword() == null || !original.getPassword().equals(updated.getPassword()))) {
244 
245             result.setPassword(new PasswordPatch.Builder().
246                     value(updated.getPassword()).
247                     resources(updated.getResources()).build());
248         }
249 
250         // 2. username
251         result.setUsername(
252                 replacePatchItem(updated.getUsername(), original.getUsername(), new StringReplacePatchItem()));
253 
254         // 3. security question / answer
255         if (updated.getSecurityQuestion() == null) {
256             result.setSecurityQuestion(null);
257             result.setSecurityAnswer(null);
258         } else if (!updated.getSecurityQuestion().equals(original.getSecurityQuestion())
259                 || StringUtils.isNotBlank(updated.getSecurityAnswer())) {
260 
261             result.setSecurityQuestion(new StringReplacePatchItem.Builder().
262                     value(updated.getSecurityQuestion()).build());
263             result.setSecurityAnswer(
264                     new StringReplacePatchItem.Builder().value(updated.getSecurityAnswer()).build());
265         }
266 
267         result.setMustChangePassword(replacePatchItem(
268                 updated.isMustChangePassword(), original.isMustChangePassword(), new BooleanReplacePatchItem()));
269 
270         // 4. roles
271         if (!incremental) {
272             original.getRoles().stream().filter(role -> !updated.getRoles().contains(role)).
273                     forEach(toRemove -> result.getRoles().add(new StringPatchItem.Builder().
274                     operation(PatchOperation.DELETE).value(toRemove).build()));
275         }
276 
277         updated.getRoles().stream().filter(role -> !original.getRoles().contains(role)).
278                 forEach(toAdd -> result.getRoles().add(new StringPatchItem.Builder().
279                 operation(PatchOperation.ADD_REPLACE).value(toAdd).build()));
280 
281         // 5. relationships
282         Map<Pair<String, String>, RelationshipTO> updatedRels =
283                 EntityTOUtils.buildRelationshipMap(updated.getRelationships());
284         Map<Pair<String, String>, RelationshipTO> originalRels =
285                 EntityTOUtils.buildRelationshipMap(original.getRelationships());
286 
287         updatedRels.entrySet().stream().
288                 filter(entry -> (!originalRels.containsKey(entry.getKey()))).
289                 forEach(entry -> result.getRelationships().add(new RelationshipUR.Builder(entry.getValue()).
290                 operation(PatchOperation.ADD_REPLACE).build()));
291 
292         if (!incremental) {
293             originalRels.keySet().stream().filter(relationship -> !updatedRels.containsKey(relationship)).
294                     forEach(key -> result.getRelationships().add(new RelationshipUR.Builder(originalRels.get(key)).
295                     operation(PatchOperation.DELETE).build()));
296         }
297 
298         // 6. memberships
299         Map<String, MembershipTO> updatedMembs = EntityTOUtils.buildMembershipMap(updated.getMemberships());
300         Map<String, MembershipTO> originalMembs = EntityTOUtils.buildMembershipMap(original.getMemberships());
301 
302         updatedMembs.forEach((key, value) -> {
303             MembershipUR membershipPatch = new MembershipUR.Builder(value.getGroupKey()).
304                     operation(PatchOperation.ADD_REPLACE).build();
305 
306             diff(value, membershipPatch);
307             result.getMemberships().add(membershipPatch);
308         });
309 
310         if (!incremental) {
311             originalMembs.keySet().stream().filter(membership -> !updatedMembs.containsKey(membership))
312                     .forEach(key -> result.getMemberships()
313                     .add(new MembershipUR.Builder(originalMembs.get(key).getGroupKey())
314                             .operation(PatchOperation.DELETE).build()));
315         }
316 
317         // 7. linked accounts
318         Map<Pair<String, String>, LinkedAccountTO> updatedAccounts =
319                 EntityTOUtils.buildLinkedAccountMap(updated.getLinkedAccounts());
320         Map<Pair<String, String>, LinkedAccountTO> originalAccounts =
321                 EntityTOUtils.buildLinkedAccountMap(original.getLinkedAccounts());
322 
323         updatedAccounts.entrySet().stream().
324                 forEachOrdered(entry -> {
325                     result.getLinkedAccounts().add(new LinkedAccountUR.Builder().
326                             operation(PatchOperation.ADD_REPLACE).
327                             linkedAccountTO(entry.getValue()).build());
328                 });
329 
330         if (!incremental) {
331             originalAccounts.keySet().stream().filter(account -> !updatedAccounts.containsKey(account)).
332                     forEach(key -> {
333                         result.getLinkedAccounts().add(new LinkedAccountUR.Builder().
334                                 operation(PatchOperation.DELETE).
335                                 linkedAccountTO(originalAccounts.get(key)).build());
336                     });
337         }
338 
339         return result;
340     }
341 
342     /**
343      * Calculate modifications needed by first in order to be equal to second.
344      *
345      * @param updated updated GroupTO
346      * @param original original GroupTO
347      * @param incremental perform incremental diff (without removing existing info)
348      * @return {@link GroupUR} containing differences
349      */
350     public static GroupUR diff(final GroupTO updated, final GroupTO original, final boolean incremental) {
351         GroupUR result = new GroupUR();
352 
353         diff(updated, original, result, incremental);
354 
355         // 1. name
356         result.setName(replacePatchItem(updated.getName(), original.getName(), new StringReplacePatchItem()));
357 
358         // 2. ownership
359         result.setUserOwner(
360                 replacePatchItem(updated.getUserOwner(), original.getUserOwner(), new StringReplacePatchItem()));
361         result.setGroupOwner(
362                 replacePatchItem(updated.getGroupOwner(), original.getGroupOwner(), new StringReplacePatchItem()));
363 
364         // 3. dynamic membership
365         result.setUDynMembershipCond(updated.getUDynMembershipCond());
366         result.getADynMembershipConds().putAll(updated.getADynMembershipConds());
367 
368         // 4. type extensions
369         result.getTypeExtensions().addAll(updated.getTypeExtensions());
370 
371         return result;
372     }
373 
374     @SuppressWarnings("unchecked")
375     public static <TO extends AnyTO, P extends AnyUR> P diff(
376             final TO updated, final TO original, final boolean incremental) {
377 
378         if (updated instanceof UserTO && original instanceof UserTO) {
379             return (P) diff((UserTO) updated, (UserTO) original, incremental);
380         } else if (updated instanceof GroupTO && original instanceof GroupTO) {
381             return (P) diff((GroupTO) updated, (GroupTO) original, incremental);
382         } else if (updated instanceof AnyObjectTO && original instanceof AnyObjectTO) {
383             return (P) diff((AnyObjectTO) updated, (AnyObjectTO) original, incremental);
384         }
385 
386         throw new IllegalArgumentException("Unsupported: " + updated.getClass().getName());
387     }
388 
389     private static Collection<Attr> patch(final Map<String, Attr> attrs, final Set<AttrPatch> attrPatches) {
390         Map<String, Attr> rwattrs = new HashMap<>(attrs);
391         attrPatches.forEach(patch -> {
392             if (patch.getAttr() == null) {
393                 LOG.warn("Invalid {} specified: {}", AttrPatch.class.getName(), patch);
394             } else {
395                 rwattrs.remove(patch.getAttr().getSchema());
396                 if (patch.getOperation() == PatchOperation.ADD_REPLACE && !patch.getAttr().getValues().isEmpty()) {
397                     rwattrs.put(patch.getAttr().getSchema(), patch.getAttr());
398                 }
399             }
400         });
401 
402         return rwattrs.values();
403     }
404 
405     private static <T extends AnyTO, K extends AnyUR> void patch(final T to, final K req, final T result) {
406         // check same key
407         if (to.getKey() == null || !to.getKey().equals(req.getKey())) {
408             throw new IllegalArgumentException(
409                     to.getClass().getSimpleName() + " and "
410                     + req.getClass().getSimpleName() + " keys must be the same");
411         }
412 
413         // 0. realm
414         if (req.getRealm() != null) {
415             result.setRealm(req.getRealm().getValue());
416         }
417 
418         // 1. auxiliary classes
419         for (StringPatchItem auxClassPatch : req.getAuxClasses()) {
420             switch (auxClassPatch.getOperation()) {
421                 case ADD_REPLACE:
422                     result.getAuxClasses().add(auxClassPatch.getValue());
423                     break;
424 
425                 case DELETE:
426                 default:
427                     result.getAuxClasses().remove(auxClassPatch.getValue());
428             }
429         }
430 
431         // 2. plain attributes
432         result.getPlainAttrs().clear();
433         result.getPlainAttrs().addAll(patch(EntityTOUtils.buildAttrMap(to.getPlainAttrs()), req.getPlainAttrs()));
434 
435         // 3. virtual attributes
436         result.getVirAttrs().clear();
437         result.getVirAttrs().addAll(req.getVirAttrs());
438 
439         // 4. resources
440         for (StringPatchItem resourcePatch : req.getResources()) {
441             switch (resourcePatch.getOperation()) {
442                 case ADD_REPLACE:
443                     result.getResources().add(resourcePatch.getValue());
444                     break;
445 
446                 case DELETE:
447                 default:
448                     result.getResources().remove(resourcePatch.getValue());
449             }
450         }
451     }
452 
453     public static AnyTO patch(final AnyTO anyTO, final AnyUR anyUR) {
454         if (anyTO instanceof UserTO) {
455             return patch((UserTO) anyTO, (UserUR) anyUR);
456         }
457         if (anyTO instanceof GroupTO) {
458             return patch((GroupTO) anyTO, (GroupUR) anyUR);
459         }
460         if (anyTO instanceof AnyObjectTO) {
461             return patch((AnyObjectTO) anyTO, (AnyObjectUR) anyUR);
462         }
463         return null;
464     }
465 
466     public static GroupTO patch(final GroupTO groupTO, final GroupUR groupUR) {
467         GroupTO result = SerializationUtils.clone(groupTO);
468         patch(groupTO, groupUR, result);
469 
470         if (groupUR.getName() != null) {
471             result.setName(groupUR.getName().getValue());
472         }
473 
474         if (groupUR.getUserOwner() != null) {
475             result.setGroupOwner(groupUR.getUserOwner().getValue());
476         }
477         if (groupUR.getGroupOwner() != null) {
478             result.setGroupOwner(groupUR.getGroupOwner().getValue());
479         }
480 
481         result.setUDynMembershipCond(groupUR.getUDynMembershipCond());
482         result.getADynMembershipConds().clear();
483         result.getADynMembershipConds().putAll(groupUR.getADynMembershipConds());
484 
485         return result;
486     }
487 
488     public static AnyObjectTO patch(final AnyObjectTO anyObjectTO, final AnyObjectUR anyObjectUR) {
489         AnyObjectTO result = SerializationUtils.clone(anyObjectTO);
490         patch(anyObjectTO, anyObjectUR, result);
491 
492         if (anyObjectUR.getName() != null) {
493             result.setName(anyObjectUR.getName().getValue());
494         }
495 
496         // 1. relationships
497         anyObjectUR.getRelationships().
498                 forEach(relPatch -> {
499                     if (relPatch.getRelationshipTO() == null) {
500                         LOG.warn("Invalid {} specified: {}", RelationshipUR.class.getName(), relPatch);
501                     } else {
502                         result.getRelationships().remove(relPatch.getRelationshipTO());
503                         if (relPatch.getOperation() == PatchOperation.ADD_REPLACE) {
504                             result.getRelationships().add(relPatch.getRelationshipTO());
505                         }
506                     }
507                 });
508 
509         // 2. memberships
510         anyObjectUR.getMemberships().forEach(membPatch -> {
511             if (membPatch.getGroup() == null) {
512                 LOG.warn("Invalid {} specified: {}", MembershipUR.class.getName(), membPatch);
513             } else {
514                 result.getMemberships().stream().
515                         filter(membership -> membPatch.getGroup().equals(membership.getGroupKey())).
516                         findFirst().ifPresent(memb -> result.getMemberships().remove(memb));
517 
518                 if (membPatch.getOperation() == PatchOperation.ADD_REPLACE) {
519                     MembershipTO newMembershipTO = new MembershipTO.Builder(membPatch.getGroup()).
520                             // 3. plain attributes
521                             plainAttrs(membPatch.getPlainAttrs()).
522                             // 4. virtual attributes
523                             virAttrs(membPatch.getVirAttrs()).
524                             build();
525 
526                     result.getMemberships().add(newMembershipTO);
527                 }
528             }
529         });
530 
531         return result;
532     }
533 
534     public static UserTO patch(final UserTO userTO, final UserUR userUR) {
535         UserTO result = SerializationUtils.clone(userTO);
536         patch(userTO, userUR, result);
537 
538         // 1. password
539         if (userUR.getPassword() != null) {
540             result.setPassword(userUR.getPassword().getValue());
541         }
542 
543         // 2. username
544         if (userUR.getUsername() != null) {
545             result.setUsername(userUR.getUsername().getValue());
546         }
547 
548         // 3. relationships
549         userUR.getRelationships().forEach(relPatch -> {
550             if (relPatch.getRelationshipTO() == null) {
551                 LOG.warn("Invalid {} specified: {}", RelationshipUR.class.getName(), relPatch);
552             } else {
553                 result.getRelationships().remove(relPatch.getRelationshipTO());
554                 if (relPatch.getOperation() == PatchOperation.ADD_REPLACE) {
555                     result.getRelationships().add(relPatch.getRelationshipTO());
556                 }
557             }
558         });
559 
560         // 4. memberships
561         userUR.getMemberships().forEach(membPatch -> {
562             if (membPatch.getGroup() == null) {
563                 LOG.warn("Invalid {} specified: {}", MembershipUR.class.getName(), membPatch);
564             } else {
565                 result.getMemberships().stream().
566                         filter(membership -> membPatch.getGroup().equals(membership.getGroupKey())).
567                         findFirst().ifPresent(memb -> result.getMemberships().remove(memb));
568 
569                 if (membPatch.getOperation() == PatchOperation.ADD_REPLACE) {
570                     MembershipTO newMembershipTO = new MembershipTO.Builder(membPatch.getGroup()).
571                             // 3. plain attributes
572                             plainAttrs(membPatch.getPlainAttrs()).
573                             // 4. virtual attributes
574                             virAttrs(membPatch.getVirAttrs()).
575                             build();
576 
577                     result.getMemberships().add(newMembershipTO);
578                 }
579             }
580         });
581 
582         // 5. roles
583         for (StringPatchItem rolePatch : userUR.getRoles()) {
584             switch (rolePatch.getOperation()) {
585                 case ADD_REPLACE:
586                     result.getRoles().add(rolePatch.getValue());
587                     break;
588 
589                 case DELETE:
590                 default:
591                     result.getRoles().remove(rolePatch.getValue());
592             }
593         }
594 
595         // 6. linked accounts
596         userUR.getLinkedAccounts().forEach(accountPatch -> {
597             if (accountPatch.getLinkedAccountTO() == null) {
598                 LOG.warn("Invalid {} specified: {}", LinkedAccountUR.class.getName(), accountPatch);
599             } else {
600                 result.getLinkedAccounts().remove(accountPatch.getLinkedAccountTO());
601                 if (accountPatch.getOperation() == PatchOperation.ADD_REPLACE) {
602                     result.getLinkedAccounts().add(accountPatch.getLinkedAccountTO());
603                 }
604             }
605         });
606 
607         return result;
608     }
609 
610     /**
611      * Add PLAIN attribute DELETE patch for those attributes of the input AnyTO without values or containing null value
612      *
613      * @param anyTO User, Group or Any Object to look for attributes with no value
614      * @param anyUR update req to enrich with DELETE statements
615      */
616     public static void cleanEmptyAttrs(final AnyTO anyTO, final AnyUR anyUR) {
617         anyUR.getPlainAttrs().addAll(anyTO.getPlainAttrs().stream().
618                 filter(AnyOperations::isEmpty).
619                 map(plainAttr -> new AttrPatch.Builder(new Attr.Builder(plainAttr.getSchema()).build()).
620                 operation(PatchOperation.DELETE).
621                 build()).collect(Collectors.toSet()));
622     }
623 
624     private static boolean isEmpty(final Attr attr) {
625         return attr.getValues().isEmpty() || NULL_SINGLETON_LIST.equals(attr.getValues());
626     }
627 }