1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
481
482
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 }