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.client.console.panels;
20  
21  import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
22  import de.agilecoders.wicket.core.markup.html.bootstrap.button.ButtonList;
23  import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
24  import de.agilecoders.wicket.core.markup.html.bootstrap.button.dropdown.DropDownAlignmentBehavior;
25  import de.agilecoders.wicket.core.markup.html.bootstrap.button.dropdown.DropDownButton;
26  import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.Comparator;
30  import java.util.HashMap;
31  import java.util.HashSet;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.stream.Collectors;
37  import java.util.stream.Stream;
38  import org.apache.commons.lang3.StringUtils;
39  import org.apache.commons.lang3.tuple.Pair;
40  import org.apache.syncope.client.console.SyncopeConsoleSession;
41  import org.apache.syncope.client.console.SyncopeWebApplication;
42  import org.apache.syncope.client.console.commons.RealmsUtils;
43  import org.apache.syncope.client.console.rest.RealmRestClient;
44  import org.apache.syncope.client.console.wicket.markup.html.WebMarkupContainerNoVeil;
45  import org.apache.syncope.client.ui.commons.Constants;
46  import org.apache.syncope.client.ui.commons.ajax.form.IndicatorAjaxFormComponentUpdatingBehavior;
47  import org.apache.syncope.common.lib.SyncopeConstants;
48  import org.apache.syncope.common.lib.to.DynRealmTO;
49  import org.apache.syncope.common.lib.to.RealmTO;
50  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
51  import org.apache.syncope.common.rest.api.beans.RealmQuery;
52  import org.apache.wicket.PageReference;
53  import org.apache.wicket.ajax.AjaxRequestTarget;
54  import org.apache.wicket.ajax.markup.html.AjaxLink;
55  import org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
56  import org.apache.wicket.event.Broadcast;
57  import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AbstractAutoCompleteRenderer;
58  import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteBehavior;
59  import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteSettings;
60  import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteTextField;
61  import org.apache.wicket.extensions.ajax.markup.html.autocomplete.IAutoCompleteRenderer;
62  import org.apache.wicket.markup.ComponentTag;
63  import org.apache.wicket.markup.html.link.AbstractLink;
64  import org.apache.wicket.markup.html.list.ListItem;
65  import org.apache.wicket.markup.html.list.ListView;
66  import org.apache.wicket.markup.html.panel.Fragment;
67  import org.apache.wicket.markup.html.panel.Panel;
68  import org.apache.wicket.model.LoadableDetachableModel;
69  import org.apache.wicket.model.Model;
70  import org.apache.wicket.model.ResourceModel;
71  import org.apache.wicket.request.Response;
72  
73  public class RealmChoicePanel extends Panel {
74  
75      private static final long serialVersionUID = -1100228004207271270L;
76  
77      protected static final String SEARCH_REALMS = "searchRealms";
78  
79      protected final RealmRestClient realmRestClient;
80  
81      protected final PageReference pageRef;
82  
83      protected final LoadableDetachableModel<List<Pair<String, RealmTO>>> realmTree;
84  
85      protected final LoadableDetachableModel<List<DynRealmTO>> dynRealmTree;
86  
87      protected final WebMarkupContainerNoVeil container;
88  
89      protected Model<RealmTO> model;
90  
91      protected final Map<String, Pair<RealmTO, List<RealmTO>>> tree;
92  
93      protected final List<AbstractLink> links = new ArrayList<>();
94  
95      protected String searchQuery;
96  
97      protected List<RealmTO> realmsChoices;
98  
99      protected final boolean fullRealmsTree;
100 
101     protected final ListView<String> breadcrumb;
102 
103     public RealmChoicePanel(
104             final String id,
105             final String base,
106             final RealmRestClient realmRestClient,
107             final PageReference pageRef) {
108 
109         super(id);
110         this.realmRestClient = realmRestClient;
111         this.pageRef = pageRef;
112 
113         tree = new HashMap<>();
114         fullRealmsTree = SyncopeWebApplication.get().fullRealmsTree(realmRestClient);
115 
116         realmTree = new LoadableDetachableModel<>() {
117 
118             private static final long serialVersionUID = -7688359318035249200L;
119 
120             @Override
121             protected List<Pair<String, RealmTO>> load() {
122                 Map<String, Pair<RealmTO, List<RealmTO>>> map = reloadRealmParentMap();
123                 Stream<Pair<String, RealmTO>> full;
124                 if (fullRealmsTree) {
125                     full = map.entrySet().stream().
126                             map(el -> Pair.of(el.getValue().getLeft().getFullPath(), el.getValue().getKey())).
127                             sorted(Comparator.comparing(Pair::getLeft));
128                 } else {
129                     full = map.entrySet().stream().
130                             map(el -> Pair.of(el.getKey(), el.getValue().getLeft()));
131                 }
132                 return full.filter(realm -> SyncopeConsoleSession.get().getSearchableRealms().stream().anyMatch(
133                         r -> realm.getValue().getFullPath().startsWith(r))).
134                         collect(Collectors.toList());
135             }
136         };
137 
138         dynRealmTree = new LoadableDetachableModel<>() {
139 
140             private static final long serialVersionUID = 5275935387613157437L;
141 
142             @Override
143             protected List<DynRealmTO> load() {
144                 List<DynRealmTO> dynRealms = realmRestClient.listDynRealms();
145                 dynRealms.sort((left, right) -> {
146                     if (left == null) {
147                         return -1;
148                     }
149                     if (right == null) {
150                         return 1;
151                     }
152                     return left.getKey().compareTo(right.getKey());
153                 });
154                 return dynRealms.stream().filter(dynRealm -> SyncopeConsoleSession.get().getSearchableRealms().stream().
155                         anyMatch(availableRealm -> SyncopeConstants.ROOT_REALM.equals(availableRealm)
156                         || dynRealm.getKey().equals(availableRealm))).collect(Collectors.toList());
157             }
158         };
159 
160         RealmTO realm = SyncopeConsoleSession.get().getRootRealm(base).map(rootRealm -> {
161             String rootRealmName = StringUtils.substringAfterLast(rootRealm, "/");
162 
163             List<RealmTO> realmTOs = realmRestClient.search(
164                     RealmsUtils.buildKeywordQuery(SyncopeConstants.ROOT_REALM.equals(rootRealm)
165                             ? SyncopeConstants.ROOT_REALM : rootRealmName)).getResult();
166 
167             return realmTOs.stream().
168                     filter(r -> rootRealm.equals(r.getFullPath())).findFirst().
169                     orElseGet(() -> {
170                         RealmTO placeholder = new RealmTO();
171                         placeholder.setName(rootRealmName);
172                         placeholder.setFullPath(rootRealm);
173                         return placeholder;
174                     });
175         }).orElseGet(() -> {
176             RealmTO root = new RealmTO();
177             root.setName(SyncopeConstants.ROOT_REALM);
178             root.setFullPath(SyncopeConstants.ROOT_REALM);
179             return root;
180         });
181 
182         model = Model.of(realm);
183         searchQuery = realm.getName();
184 
185         container = new WebMarkupContainerNoVeil("container", realmTree);
186         add(container.setOutputMarkupId(true));
187 
188         breadcrumb = new ListView<String>("breadcrumb") {
189 
190             private static final long serialVersionUID = -8746795666847966508L;
191 
192             @Override
193             protected void populateItem(final ListItem<String> item) {
194                 AjaxLink<Void> bcitem = new AjaxLink<>("bcitem") {
195 
196                     private static final long serialVersionUID = -817438685948164787L;
197 
198                     @Override
199                     public void onClick(final AjaxRequestTarget target) {
200                         realmRestClient.search(
201                                 new RealmQuery.Builder().base(item.getModelObject()).build()).getResult().stream().
202                                 findFirst().ifPresent(t -> chooseRealm(t, target));
203                     }
204                 };
205                 bcitem.setBody(Model.of(SyncopeConstants.ROOT_REALM.equals(item.getModelObject())
206                         ? SyncopeConstants.ROOT_REALM
207                         : StringUtils.substringAfterLast(item.getModelObject(), "/")));
208                 bcitem.setEnabled(!model.getObject().getFullPath().equals(item.getModelObject()));
209                 item.add(bcitem);
210             }
211         };
212         container.addOrReplace(breadcrumb.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
213         setBreadcrumb(model.getObject());
214 
215         reloadRealmsTree();
216     }
217 
218     protected void setBreadcrumb(final RealmTO realm) {
219         if (SyncopeConstants.ROOT_REALM.equals(realm.getFullPath())) {
220             breadcrumb.setList(List.of(realm.getFullPath()));
221         } else {
222             Set<String> bcitems = new HashSet<>();
223             bcitems.add(SyncopeConstants.ROOT_REALM);
224 
225             String[] split = realm.getFullPath().split("/");
226             for (int i = 1; i < split.length; i++) {
227                 StringBuilder bcitem = new StringBuilder();
228                 for (int j = 1; j <= i; j++) {
229                     bcitem.append('/').append(split[j]);
230                 }
231                 bcitems.add(bcitem.toString());
232             }
233 
234             breadcrumb.setList(bcitems.stream().sorted().collect(Collectors.toList()));
235         }
236     }
237 
238     protected void chooseRealm(final RealmTO realm, final AjaxRequestTarget target) {
239         model.setObject(realm);
240         setBreadcrumb(realm);
241         target.add(container);
242         send(pageRef.getPage(), Broadcast.EXACT, new ChosenRealm<>(realm, target));
243     }
244 
245     public void reloadRealmsTree() {
246         if (fullRealmsTree) {
247             DropDownButton realms = new DropDownButton(
248                     "realms", new ResourceModel("select", ""), new Model<>(FontAwesome5IconType.folder_open_r)) {
249 
250                 private static final long serialVersionUID = -5560086780455361131L;
251 
252                 @Override
253                 protected List<AbstractLink> newSubMenuButtons(final String buttonMarkupId) {
254                     buildRealmLinks();
255                     return RealmChoicePanel.this.links;
256                 }
257             };
258             realms.setOutputMarkupId(true);
259             realms.setAlignment(DropDownAlignmentBehavior.Alignment.RIGHT);
260             realms.setType(Buttons.Type.Menu);
261 
262             MetaDataRoleAuthorizationStrategy.authorize(realms, ENABLE, IdRepoEntitlement.REALM_SEARCH);
263             Fragment fragment = new Fragment("realmsFragment", "realmsListFragment", container);
264             fragment.addOrReplace(realms);
265             container.addOrReplace(fragment);
266         } else {
267             realmsChoices = buildRealmChoices();
268             AutoCompleteSettings settings = new AutoCompleteSettings();
269             settings.setShowCompleteListOnFocusGain(false);
270             settings.setShowListOnEmptyInput(false);
271 
272             AutoCompleteTextField<String> searchRealms =
273                     new AutoCompleteTextField<>(SEARCH_REALMS, new Model<>(), settings) {
274 
275                 private static final long serialVersionUID = -6635259975264955783L;
276 
277                 @Override
278                 protected Iterator<String> getChoices(final String input) {
279                     searchQuery = input;
280                     realmsChoices = RealmsUtils.checkInput(input)
281                             ? buildRealmChoices()
282                             : List.of();
283                     return realmsChoices.stream().map(RealmTO::getFullPath).sorted().iterator();
284                 }
285 
286                 @Override
287                 protected AutoCompleteBehavior<String> newAutoCompleteBehavior(
288                         final IAutoCompleteRenderer<String> renderer,
289                         final AutoCompleteSettings settings) {
290 
291                     return super.newAutoCompleteBehavior(new AbstractAutoCompleteRenderer<>() {
292 
293                         private static final long serialVersionUID = -4789925973199139157L;
294 
295                         @Override
296                         protected void renderChoice(
297                                 final String object,
298                                 final Response response,
299                                 final String criteria) {
300 
301                             response.write(object);
302                         }
303 
304                         @Override
305                         protected String getTextValue(final String object) {
306                             return object;
307                         }
308                     }, settings);
309                 }
310             };
311 
312             searchRealms.add(new IndicatorAjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
313 
314                 private static final long serialVersionUID = -6139318907146065915L;
315 
316                 @Override
317                 protected void onUpdate(final AjaxRequestTarget target) {
318                     realmsChoices.stream().
319                             filter(item -> item.getFullPath().equals(searchRealms.getModelObject())).
320                             findFirst().ifPresent(realm -> chooseRealm(realm, target));
321                 }
322             });
323 
324             Fragment fragment = new Fragment("realmsFragment", "realmsSearchFragment", container);
325             fragment.addOrReplace(searchRealms);
326             container.addOrReplace(fragment);
327         }
328     }
329 
330     protected void buildRealmLinks() {
331         RealmChoicePanel.this.links.clear();
332         RealmChoicePanel.this.links.add(new BootstrapAjaxLink<>(
333                 ButtonList.getButtonMarkupId(),
334                 new Model<>(),
335                 Buttons.Type.Link,
336                 new ResourceModel("realms", "Realms")) {
337 
338             private static final long serialVersionUID = -7978723352517770744L;
339 
340             @Override
341             public void onClick(final AjaxRequestTarget target) {
342             }
343 
344             @Override
345             public boolean isEnabled() {
346                 return false;
347             }
348 
349             @Override
350             protected void onComponentTag(final ComponentTag tag) {
351                 tag.put("class", "dropdown-header disabled");
352             }
353         });
354 
355         realmTree.getObject().forEach(link -> {
356             RealmChoicePanel.this.links.add(new BootstrapAjaxLink<>(
357                     ButtonList.getButtonMarkupId(),
358                     Model.of(link.getRight()),
359                     Buttons.Type.Link,
360                     new Model<>(link.getLeft())) {
361 
362                 private static final long serialVersionUID = -7978723352517770644L;
363 
364                 @Override
365                 public void onClick(final AjaxRequestTarget target) {
366                     chooseRealm(link.getRight(), target);
367                 }
368             });
369         });
370 
371         if (!dynRealmTree.getObject().isEmpty()) {
372             RealmChoicePanel.this.links.add(new BootstrapAjaxLink<>(
373                     ButtonList.getButtonMarkupId(),
374                     new Model<>(),
375                     Buttons.Type.Link,
376                     new ResourceModel("dynrealms", "Dynamic Realms")) {
377 
378                 private static final long serialVersionUID = -7978723352517770744L;
379 
380                 @Override
381                 public void onClick(final AjaxRequestTarget target) {
382                 }
383 
384                 @Override
385                 public boolean isEnabled() {
386                     return false;
387                 }
388 
389                 @Override
390                 protected void onComponentTag(final ComponentTag tag) {
391                     tag.put("class", "dropdown-header disabled");
392                 }
393             });
394 
395             dynRealmTree.getObject().forEach(dynRealmTO -> {
396                 RealmTO realm = new RealmTO();
397                 realm.setKey(dynRealmTO.getKey());
398                 realm.setName(dynRealmTO.getKey());
399                 realm.setFullPath(dynRealmTO.getKey());
400 
401                 RealmChoicePanel.this.links.add(new BootstrapAjaxLink<>(
402                         ButtonList.getButtonMarkupId(),
403                         new Model<>(),
404                         Buttons.Type.Link,
405                         new Model<>(realm.getKey())) {
406 
407                     private static final long serialVersionUID = -7978723352517770644L;
408 
409                     @Override
410                     public void onClick(final AjaxRequestTarget target) {
411                         chooseRealm(realm, target);
412                     }
413                 });
414             });
415         }
416     }
417 
418     protected List<RealmTO> buildRealmChoices() {
419         return Stream.of(
420                 realmTree.getObject().stream().map(Pair::getValue).collect(Collectors.toList()),
421                 dynRealmTree.getObject().stream().map(item -> {
422                     RealmTO realm = new RealmTO();
423                     realm.setKey(item.getKey());
424                     realm.setName(item.getKey());
425                     realm.setFullPath(item.getKey());
426                     return realm;
427                 }).collect(Collectors.toList())).flatMap(Collection::stream).
428                 collect(Collectors.toList());
429     }
430 
431     public final RealmChoicePanel reloadRealmTree(final AjaxRequestTarget target) {
432         reloadRealmsTree();
433         chooseRealm(model.getObject(), target);
434         target.add(container);
435         return this;
436     }
437 
438     public final RealmChoicePanel reloadRealmTree(final AjaxRequestTarget target, final Model<RealmTO> newModel) {
439         model = newModel;
440         reloadRealmTree(target);
441         return this;
442     }
443 
444     protected Map<String, Pair<RealmTO, List<RealmTO>>> reloadRealmParentMap() {
445         List<RealmTO> realmsToList = realmRestClient.search(fullRealmsTree
446                 ? RealmsUtils.buildRootQuery()
447                 : RealmsUtils.buildKeywordQuery(searchQuery)).getResult();
448 
449         return reloadRealmParentMap(realmsToList.stream().
450                 sorted(Comparator.comparing(RealmTO::getName)).
451                 collect(Collectors.toList()));
452     }
453 
454     protected Map<String, Pair<RealmTO, List<RealmTO>>> reloadRealmParentMap(final List<RealmTO> realms) {
455         tree.clear();
456 
457         Map<String, List<RealmTO>> cache = new HashMap<>();
458 
459         realms.forEach(realm -> {
460             List<RealmTO> children = new ArrayList<>();
461             tree.put(realm.getKey(), Pair.<RealmTO, List<RealmTO>>of(realm, children));
462 
463             if (cache.containsKey(realm.getKey())) {
464                 children.addAll(cache.get(realm.getKey()));
465                 cache.remove(realm.getKey());
466             }
467 
468             if (tree.containsKey(realm.getParent())) {
469                 tree.get(realm.getParent()).getRight().add(realm);
470             } else if (cache.containsKey(realm.getParent())) {
471                 cache.get(realm.getParent()).add(realm);
472             } else {
473                 cache.put(realm.getParent(), Stream.of(realm).collect(Collectors.toList()));
474             }
475         });
476         return tree;
477     }
478 
479     /**
480      * Gets current selected realm.
481      *
482      * @return selected realm.
483      */
484     public RealmTO getCurrentRealm() {
485         return model.getObject();
486     }
487 
488     public void setCurrentRealm(final RealmTO realmTO) {
489         model.setObject(realmTO);
490     }
491 
492     public RealmTO moveToParentRealm(final String key) {
493         for (Pair<RealmTO, List<RealmTO>> subtree : tree.values()) {
494             for (RealmTO child : subtree.getRight()) {
495                 if (child.getKey() != null && child.getKey().equals(key)) {
496                     model.setObject(subtree.getLeft());
497                     return subtree.getLeft();
498                 }
499             }
500         }
501         return null;
502     }
503 
504     public static class ChosenRealm<T> {
505 
506         protected final AjaxRequestTarget target;
507 
508         protected final T obj;
509 
510         public ChosenRealm(final T obj, final AjaxRequestTarget target) {
511             this.obj = obj;
512             this.target = target;
513         }
514 
515         public T getObj() {
516             return obj;
517         }
518 
519         public AjaxRequestTarget getTarget() {
520             return target;
521         }
522     }
523 
524     public List<AbstractLink> getLinks() {
525         return links;
526     }
527 }