blob: d4c149d6021087c7b5f3137f16dfa5792431a285 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.syncope.client.console.panels;
import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
import de.agilecoders.wicket.core.markup.html.bootstrap.button.ButtonList;
import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
import de.agilecoders.wicket.core.markup.html.bootstrap.button.dropdown.DropDownAlignmentBehavior;
import de.agilecoders.wicket.core.markup.html.bootstrap.button.dropdown.DropDownButton;
import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.syncope.client.console.SyncopeConsoleSession;
import org.apache.syncope.client.console.commons.RealmsUtils;
import org.apache.syncope.client.console.rest.RealmRestClient;
import org.apache.syncope.client.console.wicket.markup.html.WebMarkupContainerNoVeil;
import org.apache.syncope.client.ui.commons.Constants;
import org.apache.syncope.client.ui.commons.ajax.form.IndicatorAjaxFormComponentUpdatingBehavior;
import org.apache.syncope.common.lib.SyncopeConstants;
import org.apache.syncope.common.lib.to.DynRealmTO;
import org.apache.syncope.common.lib.to.RealmTO;
import org.apache.syncope.common.lib.types.IdRepoEntitlement;
import org.apache.wicket.PageReference;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
import org.apache.wicket.event.Broadcast;
import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AbstractAutoCompleteRenderer;
import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteBehavior;
import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteSettings;
import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteTextField;
import org.apache.wicket.extensions.ajax.markup.html.autocomplete.IAutoCompleteRenderer;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.AbstractLink;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.ResourceModel;
import org.apache.wicket.request.Response;
public class RealmChoicePanel extends Panel {
private static final long serialVersionUID = -1100228004207271270L;
private static final String SEARCH_REALMS = "searchRealms";
private final PageReference pageRef;
private final LoadableDetachableModel<List<Pair<String, RealmTO>>> realmTree;
private final LoadableDetachableModel<List<DynRealmTO>> dynRealmTree;
private final WebMarkupContainerNoVeil container;
private Model<RealmTO> model;
private final Collection<String> availableRealms;
private final Map<String, Pair<RealmTO, List<RealmTO>>> tree;
private final List<AbstractLink> links = new ArrayList<>();
private String searchQuery;
private List<RealmTO> realmsChoices;
private final boolean isSearchEnabled;
public RealmChoicePanel(final String id, final PageReference pageRef) {
super(id);
this.pageRef = pageRef;
availableRealms = SyncopeConsoleSession.get().getSearchableRealms();
tree = new HashMap<>();
isSearchEnabled = RealmsUtils.isSearchEnabled(SyncopeConsoleSession.get().getSearchableRealms());
realmTree = new LoadableDetachableModel<>() {
private static final long serialVersionUID = -7688359318035249200L;
@Override
protected List<Pair<String, RealmTO>> load() {
Map<String, Pair<RealmTO, List<RealmTO>>> map = reloadRealmParentMap();
List<Pair<String, RealmTO>> full;
if (isSearchEnabled) {
full = map.entrySet().stream().map(el -> Pair.of(
el.getKey(),
el.getValue().getLeft())).
collect(Collectors.toList());
} else {
full = map.entrySet().stream().
map(el -> Pair.of(
el.getValue().getLeft().getFullPath(),
el.getValue().getKey())).
sorted(Comparator.comparing(Pair::getLeft)).
collect(Collectors.toList());
}
return full.stream().filter(realm -> availableRealms.stream().anyMatch(
availableRealm -> realm.getValue().getFullPath().startsWith(availableRealm))).
collect(Collectors.toList());
}
};
dynRealmTree = new LoadableDetachableModel<>() {
private static final long serialVersionUID = 5275935387613157437L;
@Override
protected List<DynRealmTO> load() {
List<DynRealmTO> dynRealms = RealmRestClient.listDynReams();
dynRealms.sort((left, right) -> {
if (left == null) {
return -1;
} else if (right == null) {
return 1;
} else {
return left.getKey().compareTo(right.getKey());
}
});
return dynRealms.stream().filter(dynRealm -> availableRealms.stream().
anyMatch(availableRealm -> SyncopeConstants.ROOT_REALM.equals(availableRealm)
|| dynRealm.getKey().equals(availableRealm))).collect(Collectors.toList());
}
};
RealmTO realmTO = SyncopeConsoleSession.get().getRootRealm().map(rootRealm -> {
String rootRealmName = StringUtils.substringAfterLast(rootRealm, "/");
List<RealmTO> realmTOs = RealmRestClient.search(
RealmsUtils.buildQuery(SyncopeConstants.ROOT_REALM.equals(rootRealm)
? SyncopeConstants.ROOT_REALM : rootRealmName)).getResult();
return realmTOs.stream().filter(realm -> rootRealm.equals(realm.getFullPath())).findFirst().
orElseGet(() -> {
RealmTO placeholder = new RealmTO();
placeholder.setName(rootRealmName);
placeholder.setFullPath(rootRealm);
return placeholder;
});
}).orElseGet(RealmTO::new);
model = Model.of(realmTO);
searchQuery = realmTO.getName();
container = new WebMarkupContainerNoVeil("container", realmTree);
container.setOutputMarkupId(true);
add(container);
reloadRealmTree();
}
public final void reloadRealmTree() {
Label realmLabel = new Label("realmLabel", new Model<>());
realmLabel.setOutputMarkupId(true);
container.addOrReplace(realmLabel);
if (StringUtils.startsWith(model.getObject().getFullPath(), SyncopeConstants.ROOT_REALM)) {
realmLabel.setDefaultModel(new ResourceModel("realmLabel", "Realm"));
} else {
realmLabel.setDefaultModel(new ResourceModel("dynRealmLabel", "Dynamic Realm"));
}
Label label = new Label("realm", RealmsUtils.getFullPath(model.getObject().getFullPath()));
label.setOutputMarkupId(true);
container.addOrReplace(label);
if (isSearchEnabled) {
realmsChoices = buildRealmChoices();
final AutoCompleteSettings settings = new AutoCompleteSettings();
settings.setShowCompleteListOnFocusGain(false);
settings.setShowListOnEmptyInput(false);
final AutoCompleteTextField<String> searchRealms =
new AutoCompleteTextField<>(SEARCH_REALMS, new Model<>(), settings) {
private static final long serialVersionUID = -6635259975264955783L;
@Override
protected Iterator<String> getChoices(final String input) {
searchQuery = input;
realmsChoices = RealmsUtils.checkInput(input)
? buildRealmChoices()
: List.of();
return realmsChoices.stream().
map(RealmTO::getFullPath).sorted().collect(Collectors.toList()).iterator();
}
@Override
protected AutoCompleteBehavior<String> newAutoCompleteBehavior(
final IAutoCompleteRenderer<String> renderer,
final AutoCompleteSettings settings) {
return super.newAutoCompleteBehavior(new AbstractAutoCompleteRenderer<>() {
private static final long serialVersionUID = -4789925973199139157L;
@Override
protected void renderChoice(
final String object,
final Response response,
final String criteria) {
response.write(object);
}
@Override
protected String getTextValue(final String object) {
return object;
}
}, settings);
}
};
searchRealms.add(new IndicatorAjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
private static final long serialVersionUID = -6139318907146065915L;
@Override
protected void onUpdate(final AjaxRequestTarget target) {
realmsChoices.stream().filter(
item -> item.getFullPath().equals(searchRealms.getModelObject())).
findFirst().ifPresent(realm -> {
model.setObject(realm);
label.setDefaultModelObject(model.getObject().getFullPath());
realmLabel.setDefaultModel(new ResourceModel("realmLabel", "Realm"));
target.add(label);
send(pageRef.getPage(), Broadcast.EXACT, new ChosenRealm<>(realm, target));
});
}
});
Fragment fragment = new Fragment("realmsFragment", "realmsSearchFragment", container);
fragment.addOrReplace(searchRealms);
container.addOrReplace(fragment);
} else {
DropDownButton realms = new DropDownButton(
"realms", new ResourceModel("select", ""), new Model<>(FontAwesome5IconType.folder_open_r)) {
private static final long serialVersionUID = -5560086780455361131L;
@Override
protected List<AbstractLink> newSubMenuButtons(final String buttonMarkupId) {
buildRealmLinks(label, realmLabel);
return RealmChoicePanel.this.links;
}
};
realms.setOutputMarkupId(true);
realms.setAlignment(DropDownAlignmentBehavior.Alignment.RIGHT);
realms.setType(Buttons.Type.Menu);
MetaDataRoleAuthorizationStrategy.authorize(realms, ENABLE, IdRepoEntitlement.REALM_LIST);
Fragment fragment = new Fragment("realmsFragment", "realmsListFragment", container);
fragment.addOrReplace(realms);
container.addOrReplace(fragment);
}
}
private void buildRealmLinks(final Label label, final Label realmLabel) {
RealmChoicePanel.this.links.clear();
RealmChoicePanel.this.links.add(new BootstrapAjaxLink<>(
ButtonList.getButtonMarkupId(),
new Model<>(),
Buttons.Type.Link,
new ResourceModel("realms", "Realms")) {
private static final long serialVersionUID = -7978723352517770744L;
@Override
public void onClick(final AjaxRequestTarget target) {
}
@Override
public boolean isEnabled() {
return false;
}
@Override
protected void onComponentTag(final ComponentTag tag) {
tag.put("class", "dropdown-header disabled");
}
});
realmTree.getObject().forEach(link -> {
RealmChoicePanel.this.links.add(new BootstrapAjaxLink<>(
ButtonList.getButtonMarkupId(),
Model.of(link.getRight()),
Buttons.Type.Link,
new Model<>(link.getLeft())) {
private static final long serialVersionUID = -7978723352517770644L;
@Override
public void onClick(final AjaxRequestTarget target) {
model.setObject(link.getRight());
label.setDefaultModelObject(model.getObject().getFullPath());
realmLabel.setDefaultModel(new ResourceModel("realmLabel", "Realm"));
target.add(label);
send(pageRef.getPage(), Broadcast.EXACT, new ChosenRealm<>(link.getRight(), target));
}
});
});
if (!dynRealmTree.getObject().isEmpty()) {
RealmChoicePanel.this.links.add(new BootstrapAjaxLink<>(
ButtonList.getButtonMarkupId(),
new Model<>(),
Buttons.Type.Link,
new ResourceModel("dynrealms", "Dynamic Realms")) {
private static final long serialVersionUID = -7978723352517770744L;
@Override
public void onClick(final AjaxRequestTarget target) {
}
@Override
public boolean isEnabled() {
return false;
}
@Override
protected void onComponentTag(final ComponentTag tag) {
tag.put("class", "dropdown-header disabled");
}
});
dynRealmTree.getObject().forEach(dynRealmTO -> {
final RealmTO realmTO = new RealmTO();
realmTO.setKey(dynRealmTO.getKey());
realmTO.setName(dynRealmTO.getKey());
realmTO.setFullPath(dynRealmTO.getKey());
RealmChoicePanel.this.links.add(new BootstrapAjaxLink<>(
ButtonList.getButtonMarkupId(),
new Model<>(),
Buttons.Type.Link,
new Model<>(realmTO.getKey())) {
private static final long serialVersionUID = -7978723352517770644L;
@Override
public void onClick(final AjaxRequestTarget target) {
model.setObject(realmTO);
label.setDefaultModelObject(realmTO.getKey());
realmLabel.setDefaultModel(new ResourceModel("dynRealmLabel", "Dynamic Realm"));
target.add(label);
send(pageRef.getPage(), Broadcast.EXACT, new ChosenRealm<>(realmTO, target));
}
});
});
}
}
public final RealmChoicePanel reloadRealmTree(final AjaxRequestTarget target) {
reloadRealmTree();
target.add(container);
return this;
}
public final RealmChoicePanel reloadRealmTree(final AjaxRequestTarget target, final Model<RealmTO> newModel) {
model = newModel;
reloadRealmTree(target);
return this;
}
private Map<String, Pair<RealmTO, List<RealmTO>>> reloadRealmParentMap() {
List<RealmTO> realmsToList = isSearchEnabled
? RealmRestClient.search(RealmsUtils.buildQuery(searchQuery)).getResult()
: RealmRestClient.list();
return reloadRealmParentMap(realmsToList.stream().
sorted(Comparator.comparing(RealmTO::getName)).
collect(Collectors.toList()));
}
private Map<String, Pair<RealmTO, List<RealmTO>>> reloadRealmParentMap(final List<RealmTO> realms) {
tree.clear();
Map<String, List<RealmTO>> cache = new HashMap<>();
realms.forEach(realm -> {
List<RealmTO> children = new ArrayList<>();
tree.put(realm.getKey(), Pair.<RealmTO, List<RealmTO>>of(realm, children));
if (cache.containsKey(realm.getKey())) {
children.addAll(cache.get(realm.getKey()));
cache.remove(realm.getKey());
}
if (tree.containsKey(realm.getParent())) {
tree.get(realm.getParent()).getRight().add(realm);
} else if (cache.containsKey(realm.getParent())) {
cache.get(realm.getParent()).add(realm);
} else {
cache.put(realm.getParent(), Stream.of(realm).collect(Collectors.toList()));
}
});
return tree;
}
private List<RealmTO> buildRealmChoices() {
return Stream.of(
realmTree.getObject().stream().map(Pair::getValue).collect(Collectors.toList()),
dynRealmTree.getObject().stream().map(
item -> {
final RealmTO realmTO = new RealmTO();
realmTO.setKey(item.getKey());
realmTO.setName(item.getKey());
realmTO.setFullPath(item.getKey());
return realmTO;
}).collect(Collectors.toList())).flatMap(Collection::stream).
collect(Collectors.toList());
}
/**
* Gets current selected realm.
*
* @return selected realm.
*/
public RealmTO getCurrentRealm() {
return model.getObject();
}
public void setCurrentRealm(final RealmTO realmTO) {
model.setObject(realmTO);
}
public RealmTO moveToParentRealm(final String key) {
for (Pair<RealmTO, List<RealmTO>> subtree : tree.values()) {
for (RealmTO child : subtree.getRight()) {
if (child.getKey() != null && child.getKey().equals(key)) {
model.setObject(subtree.getLeft());
return subtree.getLeft();
}
}
}
return null;
}
public static class ChosenRealm<T> {
private final AjaxRequestTarget target;
private final T obj;
public ChosenRealm(final T obj, final AjaxRequestTarget target) {
this.obj = obj;
this.target = target;
}
public T getObj() {
return obj;
}
public AjaxRequestTarget getTarget() {
return target;
}
}
public List<AbstractLink> getLinks() {
return links;
}
}