blob: c6c2250cff1336aa65b92a927146bfb7d42c6ea1 [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.ui.commons.markup.html.form;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.syncope.client.ui.commons.Constants;
import org.apache.syncope.client.ui.commons.ajax.form.IndicatorAjaxFormComponentUpdatingBehavior;
import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
import org.apache.syncope.common.lib.to.UserTO;
import org.apache.wicket.Component;
import org.apache.wicket.Session;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.extensions.markup.html.form.palette.Palette;
import org.apache.wicket.extensions.markup.html.form.palette.component.Recorder;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.ResourceModel;
import org.apache.wicket.util.string.Strings;
public class AjaxPalettePanel<T extends Serializable> extends AbstractFieldPanel<List<T>> {
private static final long serialVersionUID = 7738499668258805567L;
protected Palette<T> palette;
protected final Model<String> queryFilter = new Model<>(StringUtils.EMPTY);
protected final List<T> availableBefore = new ArrayList<>();
private final LoadableDetachableModel<List<T>> choicesModel;
public AjaxPalettePanel(
final String id, final IModel<List<T>> model, final Builder.Query<T> query, final Builder<T> builder) {
super(id, builder.name == null ? id : builder.name, model);
choicesModel = new PaletteLoadableDetachableModel(builder) {
private static final long serialVersionUID = -108100712154481840L;
@Override
protected List<T> getChoices() {
return query.execute(queryFilter.getObject());
}
};
initialize(model, builder);
}
public AjaxPalettePanel(
final String id, final IModel<List<T>> model, final IModel<List<T>> choices, final Builder<T> builder) {
super(id, builder.name == null ? id : builder.name, model);
choicesModel = new PaletteLoadableDetachableModel(builder) {
private static final long serialVersionUID = -108100712154481840L;
@Override
protected List<T> getChoices() {
return builder.filtered
? getFilteredList(choices.getObject(), queryFilter.getObject().replaceAll("\\*", "\\.\\*"))
: choices.getObject();
}
};
initialize(model, builder);
}
protected void initialize(final IModel<List<T>> model, final Builder<T> builder) {
setOutputMarkupId(true);
palette = buildPalette(model, builder);
add(palette.setLabel(new ResourceModel(name)).setOutputMarkupId(true));
Form<?> form = new Form<>("form");
add(form.setEnabled(builder.filtered).setVisible(builder.filtered));
queryFilter.setObject(builder.filter);
AjaxTextFieldPanel filter = new AjaxTextFieldPanel("filter", "filter", queryFilter, false);
form.add(filter.hideLabel().setOutputMarkupId(true));
AjaxButton search = new AjaxButton("search") {
private static final long serialVersionUID = 8390605330558248736L;
@Override
protected void onSubmit(final AjaxRequestTarget target) {
if (builder.warnIfEmptyFilter && StringUtils.isEmpty(queryFilter.getObject())) {
Session.get().info(getString("nomatch"));
((BaseWebPage) getPage()).getNotificationPanel().refresh(target);
}
target.add(palette);
}
};
search.setOutputMarkupId(true);
form.add(search);
}
protected Palette<T> buildPalette(final IModel<List<T>> model, final Builder<T> builder) {
return new NonI18nPalette<T>(
"paletteField", model, choicesModel, builder.renderer, 8, builder.allowOrder, builder.allowMoveAll) {
private static final long serialVersionUID = -3074655279011678437L;
@Override
protected Component newAvailableHeader(final String componentId) {
return new Label(componentId, new ResourceModel("palette.available", builder.availableLabel));
}
@Override
protected Component newSelectedHeader(final String componentId) {
return new Label(componentId, new ResourceModel("palette.selected", builder.selectedLabel));
}
@Override
protected Recorder<T> newRecorderComponent() {
Recorder<T> recorder = new Recorder<T>("recorder", this) {
private static final long serialVersionUID = -9169109967480083523L;
@Override
public List<T> getUnselectedList() {
IChoiceRenderer<? super T> renderer = getChoiceRenderer();
Collection<? extends T> choices = getChoices();
List<String> ids = builder.idExtractor.apply(getValue()).collect(Collectors.toList());
List<T> unselected = new ArrayList<>(choices.size());
choices.forEach(choice -> {
if (!ids.contains(renderer.getIdValue(choice, 0))) {
unselected.add(choice);
}
});
return unselected;
}
@Override
public List<T> getSelectedList() {
IChoiceRenderer<? super T> renderer = getChoiceRenderer();
Collection<? extends T> choices = getChoices();
// reduce number of method calls by building a lookup table
Map<T, String> idForChoice = choices.stream().collect(Collectors.toMap(
Function.identity(), choice -> renderer.getIdValue(choice, 0), (c1, c2) -> c1));
List<T> selected = new ArrayList<>(choices.size());
builder.idExtractor.apply(getValue()).forEach(id -> {
for (T choice : choices) {
if (id.equals(idForChoice.get(choice))) {
selected.add(choice);
break;
}
}
});
return selected;
}
};
recorder.add(new IndicatorAjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
private static final long serialVersionUID = -6139318907146065915L;
@Override
protected void onUpdate(final AjaxRequestTarget target) {
processInput();
Optional.ofNullable(builder.event).ifPresent(e -> e.apply(target));
}
});
return recorder;
}
@Override
protected Map<String, String> getAdditionalAttributes(final Object choice) {
return builder.additionalAttributes == null
? super.getAdditionalAttributes(choice)
: builder.additionalAttributes.apply(choice);
}
};
}
public Recorder<T> getRecorderComponent() {
return palette.getRecorderComponent();
}
public LoadableDetachableModel<List<T>> getChoicesModel() {
return choicesModel;
}
@Override
public AjaxPalettePanel<T> setModelObject(final List<T> object) {
palette.setDefaultModelObject(object);
return this;
}
public Collection<T> getModelCollection() {
return palette.getModelCollection();
}
public void reload(final AjaxRequestTarget target) {
target.add(palette);
}
@Override
public AbstractFieldPanel<List<T>> setReadOnly(final boolean readOnly) {
palette.setEnabled(!readOnly);
return this;
}
@Override
public AbstractFieldPanel<List<T>> setRequired(final boolean required) {
palette.setRequired(required);
return super.setRequired(required);
}
public static class Builder<T extends Serializable> implements Serializable {
private static final long serialVersionUID = 991248996001040352L;
protected String name;
protected IChoiceRenderer<T> renderer = new SelectChoiceRenderer<>();
protected boolean allowOrder;
protected boolean allowMoveAll;
protected String selectedLabel;
protected String availableLabel;
protected boolean filtered;
protected String filter = "*";
protected boolean warnIfEmptyFilter = true;
protected Function<String, Stream<String>> idExtractor =
(Function<String, Stream<String>> & Serializable) input -> Stream.of(Strings.split(input, ','));
protected Function<AjaxRequestTarget, Boolean> event;
protected Function<Object, Map<String, String>> additionalAttributes;
public Builder<T> setName(final String name) {
this.name = name;
return this;
}
public Builder<T> setAllowOrder(final boolean allowOrder) {
this.allowOrder = allowOrder;
return this;
}
public Builder<T> setAllowMoveAll(final boolean allowMoveAll) {
this.allowMoveAll = allowMoveAll;
return this;
}
public Builder<T> setSelectedLabel(final String selectedLabel) {
this.selectedLabel = selectedLabel;
return this;
}
public Builder<T> setAvailableLabel(final String availableLabel) {
this.availableLabel = availableLabel;
return this;
}
public Builder<T> setRenderer(final IChoiceRenderer<T> renderer) {
this.renderer = renderer;
return this;
}
public Builder<T> withFilter() {
this.filtered = true;
return this;
}
public Builder<T> withFilter(final String defaultFilter) {
this.filtered = true;
this.filter = defaultFilter;
return this;
}
public Builder<T> warnIfEmptyFilter(final boolean warnIfEmptyFilter) {
this.warnIfEmptyFilter = warnIfEmptyFilter;
return this;
}
public Builder<T> idExtractor(final Function<String, Stream<String>> idExtractor) {
this.idExtractor = idExtractor;
return this;
}
public Builder<T> event(final Function<AjaxRequestTarget, Boolean> event) {
this.event = event;
return this;
}
public Builder<T> additionalAttributes(final Function<Object, Map<String, String>> additionalAttributes) {
this.additionalAttributes = additionalAttributes;
return this;
}
public AjaxPalettePanel<T> build(final String id, final IModel<List<T>> model, final IModel<List<T>> choices) {
return new AjaxPalettePanel<>(id, model, choices, this);
}
public AjaxPalettePanel<T> build(final String id, final IModel<List<T>> model, final Query<T> choices) {
return new AjaxPalettePanel<>(id, model, choices, this);
}
public abstract static class Query<T extends Serializable> implements Serializable {
private static final long serialVersionUID = 3582312993557742858L;
public abstract List<T> execute(String filter);
}
}
protected abstract class PaletteLoadableDetachableModel extends LoadableDetachableModel<List<T>> {
private static final long serialVersionUID = -7745220313769774616L;
protected final Builder<T> builder;
public PaletteLoadableDetachableModel(final Builder<T> builder) {
this.builder = builder;
}
protected abstract List<T> getChoices();
@Override
protected List<T> load() {
List<T> selected = availableBefore.isEmpty()
? new ArrayList<>(palette.getModelCollection())
: getSelectedList(availableBefore);
availableBefore.clear();
availableBefore.addAll(ListUtils.sum(selected, getChoices()));
return availableBefore;
}
protected List<T> getSelectedList(final Collection<T> choices) {
IChoiceRenderer<? super T> renderer = palette.getChoiceRenderer();
Map<T, String> idForChoice = choices.stream().collect(Collectors.toMap(
Function.identity(), choice -> renderer.getIdValue(choice, 0), (c1, c2) -> c1));
List<T> selected = new ArrayList<>();
builder.idExtractor.apply(palette.getRecorderComponent().getValue()).forEach(id -> {
Iterator<T> iter = choices.iterator();
boolean found = false;
while (!found && iter.hasNext()) {
T choice = iter.next();
if (id.equals(idForChoice.get(choice))) {
selected.add(choice);
found = true;
}
}
});
return selected;
}
protected List<T> getFilteredList(final Collection<T> choices, final String filter) {
IChoiceRenderer<? super T> renderer = palette.getChoiceRenderer();
Map<T, String> idForChoice = choices.stream().collect(Collectors.toMap(
Function.identity(), choice -> renderer.getIdValue(choice, 0), (c1, c2) -> c1));
Pattern pattern = Pattern.compile(filter, Pattern.CASE_INSENSITIVE);
List<T> filtered = new ArrayList<>(choices.size());
choices.forEach(choice -> {
if (pattern.matcher(idForChoice.get(choice)).matches()) {
filtered.add(choice);
}
});
return filtered;
}
}
public static class UpdateActionEvent {
private final UserTO item;
private final AjaxRequestTarget target;
public UpdateActionEvent(final UserTO item, final AjaxRequestTarget target) {
this.item = item;
this.target = target;
}
public UserTO getItem() {
return item;
}
public AjaxRequestTarget getTarget() {
return target;
}
}
}