1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.syncope.client.ui.commons.markup.html.form;
20
21 import java.io.Serializable;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Optional;
28 import java.util.function.Function;
29 import java.util.regex.Pattern;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
32 import org.apache.commons.collections4.ListUtils;
33 import org.apache.commons.lang3.StringUtils;
34 import org.apache.syncope.client.ui.commons.Constants;
35 import org.apache.syncope.client.ui.commons.ajax.form.IndicatorAjaxFormComponentUpdatingBehavior;
36 import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
37 import org.apache.syncope.common.lib.to.UserTO;
38 import org.apache.wicket.Component;
39 import org.apache.wicket.Session;
40 import org.apache.wicket.ajax.AjaxRequestTarget;
41 import org.apache.wicket.ajax.markup.html.form.AjaxButton;
42 import org.apache.wicket.extensions.markup.html.form.palette.Palette;
43 import org.apache.wicket.extensions.markup.html.form.palette.component.Recorder;
44 import org.apache.wicket.markup.html.basic.Label;
45 import org.apache.wicket.markup.html.form.Form;
46 import org.apache.wicket.markup.html.form.IChoiceRenderer;
47 import org.apache.wicket.model.IModel;
48 import org.apache.wicket.model.LoadableDetachableModel;
49 import org.apache.wicket.model.Model;
50 import org.apache.wicket.model.ResourceModel;
51 import org.apache.wicket.util.string.Strings;
52 import org.danekja.java.util.function.serializable.SerializableFunction;
53
54 public class AjaxPalettePanel<T extends Serializable> extends AbstractFieldPanel<List<T>> {
55
56 private static final long serialVersionUID = 7738499668258805567L;
57
58 protected Palette<T> palette;
59
60 protected final Model<String> queryFilter = new Model<>(StringUtils.EMPTY);
61
62 protected final List<T> availableBefore = new ArrayList<>();
63
64 private final LoadableDetachableModel<List<T>> choicesModel;
65
66 public AjaxPalettePanel(
67 final String id, final IModel<List<T>> model, final Builder.Query<T> query, final Builder<T> builder) {
68
69 super(id, builder.name == null ? id : builder.name, model);
70
71 choicesModel = new PaletteLoadableDetachableModel(builder) {
72
73 private static final long serialVersionUID = -108100712154481840L;
74
75 @Override
76 protected List<T> getChoices() {
77 return query.execute(queryFilter.getObject());
78 }
79 };
80 initialize(model, builder);
81 }
82
83 public AjaxPalettePanel(
84 final String id, final IModel<List<T>> model, final IModel<List<T>> choices, final Builder<T> builder) {
85 super(id, builder.name == null ? id : builder.name, model);
86
87 choicesModel = new PaletteLoadableDetachableModel(builder) {
88
89 private static final long serialVersionUID = -108100712154481840L;
90
91 @Override
92 protected List<T> getChoices() {
93 return builder.filtered
94 ? getFilteredList(choices.getObject(), queryFilter.getObject().replaceAll("\\*", "\\.\\*"))
95 : choices.getObject();
96 }
97 };
98 initialize(model, builder);
99 }
100
101 protected void initialize(final IModel<List<T>> model, final Builder<T> builder) {
102 setOutputMarkupId(true);
103
104 palette = buildPalette(model, builder);
105 add(palette.setLabel(new ResourceModel(name)).setOutputMarkupId(true));
106
107 Form<?> form = new Form<>("form");
108 add(form.setEnabled(builder.filtered).setVisible(builder.filtered));
109
110 queryFilter.setObject(builder.filter);
111 AjaxTextFieldPanel filter = new AjaxTextFieldPanel("filter", "filter", queryFilter, false);
112 form.add(filter.hideLabel().setOutputMarkupId(true));
113
114 AjaxButton search = new AjaxButton("search") {
115
116 private static final long serialVersionUID = 8390605330558248736L;
117
118 @Override
119 protected void onSubmit(final AjaxRequestTarget target) {
120 if (builder.warnIfEmptyFilter && StringUtils.isEmpty(queryFilter.getObject())) {
121 Session.get().info(getString("nomatch"));
122 ((BaseWebPage) getPage()).getNotificationPanel().refresh(target);
123 }
124
125 target.add(palette);
126 }
127 };
128 search.setOutputMarkupId(true);
129 form.add(search);
130 }
131
132 protected Palette<T> buildPalette(final IModel<List<T>> model, final Builder<T> builder) {
133 return new NonI18nPalette<>(
134 "paletteField", model, choicesModel, builder.renderer, 8, builder.allowOrder, builder.allowMoveAll) {
135
136 private static final long serialVersionUID = -3074655279011678437L;
137
138 @Override
139 protected Component newAvailableHeader(final String componentId) {
140 return new Label(componentId, new ResourceModel("palette.available", builder.availableLabel));
141 }
142
143 @Override
144 protected Component newSelectedHeader(final String componentId) {
145 return new Label(componentId, new ResourceModel("palette.selected", builder.selectedLabel));
146 }
147
148 @Override
149 protected Recorder<T> newRecorderComponent() {
150 Recorder<T> recorder = new Recorder<>("recorder", this) {
151
152 private static final long serialVersionUID = -9169109967480083523L;
153
154 @Override
155 public List<T> getUnselectedList() {
156 IChoiceRenderer<? super T> renderer = getChoiceRenderer();
157 Collection<? extends T> choices = getChoices();
158
159 List<String> ids = builder.idExtractor.apply(getValue()).collect(Collectors.toList());
160 List<T> unselected = new ArrayList<>(choices.size());
161 choices.forEach(choice -> {
162 if (!ids.contains(renderer.getIdValue(choice, 0))) {
163 unselected.add(choice);
164 }
165 });
166
167 return unselected;
168 }
169
170 @Override
171 public List<T> getSelectedList() {
172 IChoiceRenderer<? super T> renderer = getChoiceRenderer();
173 Collection<? extends T> choices = getChoices();
174
175
176 Map<T, String> idForChoice = choices.stream().collect(Collectors.toMap(
177 Function.identity(), choice -> renderer.getIdValue(choice, 0), (c1, c2) -> c1));
178
179 List<T> selected = new ArrayList<>(choices.size());
180 builder.idExtractor.apply(getValue()).forEach(id -> {
181 for (T choice : choices) {
182 if (id.equals(idForChoice.get(choice))) {
183 selected.add(choice);
184 break;
185 }
186 }
187 });
188
189 return selected;
190 }
191 };
192 recorder.add(new IndicatorAjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
193
194 private static final long serialVersionUID = -6139318907146065915L;
195
196 @Override
197 protected void onUpdate(final AjaxRequestTarget target) {
198 processInput();
199 Optional.ofNullable(builder.event).ifPresent(e -> e.apply(target));
200 }
201 });
202
203 return recorder;
204 }
205
206 @Override
207 protected Map<String, String> getAdditionalAttributes(final Object choice) {
208 return builder.additionalAttributes == null
209 ? super.getAdditionalAttributes(choice)
210 : builder.additionalAttributes.apply(choice);
211 }
212 };
213 }
214
215 public Recorder<T> getRecorderComponent() {
216 return palette.getRecorderComponent();
217 }
218
219 public LoadableDetachableModel<List<T>> getChoicesModel() {
220 return choicesModel;
221 }
222
223 @Override
224 public AjaxPalettePanel<T> setModelObject(final List<T> object) {
225 palette.setDefaultModelObject(object);
226 return this;
227 }
228
229 public Collection<T> getModelCollection() {
230 return palette.getModelCollection();
231 }
232
233 public void reload(final AjaxRequestTarget target) {
234 target.add(palette);
235 }
236
237 @Override
238 public AbstractFieldPanel<List<T>> setReadOnly(final boolean readOnly) {
239 palette.setEnabled(!readOnly);
240 return this;
241 }
242
243 @Override
244 public AbstractFieldPanel<List<T>> setRequired(final boolean required) {
245 palette.setRequired(required);
246 return super.setRequired(required);
247 }
248
249 public static class Builder<T extends Serializable> implements Serializable {
250
251 private static final long serialVersionUID = 991248996001040352L;
252
253 protected String name;
254
255 protected IChoiceRenderer<T> renderer = new SelectChoiceRenderer<>();
256
257 protected boolean allowOrder;
258
259 protected boolean allowMoveAll;
260
261 protected String selectedLabel;
262
263 protected String availableLabel;
264
265 protected boolean filtered;
266
267 protected String filter = "*";
268
269 protected boolean warnIfEmptyFilter = true;
270
271 protected SerializableFunction<String, Stream<String>> idExtractor =
272 input -> Stream.of(Strings.split(input, ','));
273
274 protected SerializableFunction<AjaxRequestTarget, Boolean> event;
275
276 protected SerializableFunction<Object, Map<String, String>> additionalAttributes;
277
278 public Builder<T> setName(final String name) {
279 this.name = name;
280 return this;
281 }
282
283 public Builder<T> setAllowOrder(final boolean allowOrder) {
284 this.allowOrder = allowOrder;
285 return this;
286 }
287
288 public Builder<T> setAllowMoveAll(final boolean allowMoveAll) {
289 this.allowMoveAll = allowMoveAll;
290 return this;
291 }
292
293 public Builder<T> setSelectedLabel(final String selectedLabel) {
294 this.selectedLabel = selectedLabel;
295 return this;
296 }
297
298 public Builder<T> setAvailableLabel(final String availableLabel) {
299 this.availableLabel = availableLabel;
300 return this;
301 }
302
303 public Builder<T> setRenderer(final IChoiceRenderer<T> renderer) {
304 this.renderer = renderer;
305 return this;
306 }
307
308 public Builder<T> withFilter() {
309 this.filtered = true;
310 return this;
311 }
312
313 public Builder<T> withFilter(final String defaultFilter) {
314 this.filtered = true;
315 this.filter = defaultFilter;
316 return this;
317 }
318
319 public Builder<T> warnIfEmptyFilter(final boolean warnIfEmptyFilter) {
320 this.warnIfEmptyFilter = warnIfEmptyFilter;
321 return this;
322 }
323
324 public Builder<T> idExtractor(final SerializableFunction<String, Stream<String>> idExtractor) {
325 this.idExtractor = idExtractor;
326 return this;
327 }
328
329 public Builder<T> event(final SerializableFunction<AjaxRequestTarget, Boolean> event) {
330 this.event = event;
331 return this;
332 }
333
334 public Builder<T> additionalAttributes(
335 final SerializableFunction<Object, Map<String, String>> additionalAttributes) {
336
337 this.additionalAttributes = additionalAttributes;
338 return this;
339 }
340
341 public AjaxPalettePanel<T> build(final String id, final IModel<List<T>> model, final IModel<List<T>> choices) {
342 return new AjaxPalettePanel<>(id, model, choices, this);
343 }
344
345 public AjaxPalettePanel<T> build(final String id, final IModel<List<T>> model, final Query<T> choices) {
346 return new AjaxPalettePanel<>(id, model, choices, this);
347 }
348
349 public abstract static class Query<T extends Serializable> implements Serializable {
350
351 private static final long serialVersionUID = 3582312993557742858L;
352
353 public abstract List<T> execute(String filter);
354 }
355 }
356
357 protected abstract class PaletteLoadableDetachableModel extends LoadableDetachableModel<List<T>> {
358
359 private static final long serialVersionUID = -7745220313769774616L;
360
361 protected final Builder<T> builder;
362
363 public PaletteLoadableDetachableModel(final Builder<T> builder) {
364 this.builder = builder;
365 }
366
367 protected abstract List<T> getChoices();
368
369 @Override
370 protected List<T> load() {
371 List<T> selected = availableBefore.isEmpty()
372 ? new ArrayList<>(palette.getModelCollection())
373 : getSelectedList(availableBefore);
374
375 availableBefore.clear();
376 availableBefore.addAll(ListUtils.sum(selected, getChoices()));
377 return availableBefore;
378 }
379
380 protected List<T> getSelectedList(final Collection<T> choices) {
381 IChoiceRenderer<? super T> renderer = palette.getChoiceRenderer();
382
383 Map<T, String> idForChoice = choices.stream().collect(Collectors.toMap(
384 Function.identity(), choice -> renderer.getIdValue(choice, 0), (c1, c2) -> c1));
385
386 List<T> selected = new ArrayList<>();
387 builder.idExtractor.apply(palette.getRecorderComponent().getValue()).forEach(id -> {
388 Iterator<T> iter = choices.iterator();
389 boolean found = false;
390 while (!found && iter.hasNext()) {
391 T choice = iter.next();
392 if (id.equals(idForChoice.get(choice))) {
393 selected.add(choice);
394 found = true;
395 }
396 }
397 });
398
399 return selected;
400 }
401
402 protected List<T> getFilteredList(final Collection<T> choices, final String filter) {
403 IChoiceRenderer<? super T> renderer = palette.getChoiceRenderer();
404
405 Map<T, String> idForChoice = choices.stream().collect(Collectors.toMap(
406 Function.identity(), choice -> renderer.getIdValue(choice, 0), (c1, c2) -> c1));
407
408 Pattern pattern = Pattern.compile(filter, Pattern.CASE_INSENSITIVE);
409
410 return choices.stream().
411 filter(choice -> pattern.matcher(idForChoice.get(choice)).matches()).
412 collect(Collectors.toList());
413 }
414 }
415
416 public static class UpdateActionEvent {
417
418 private final UserTO item;
419
420 private final AjaxRequestTarget target;
421
422 public UpdateActionEvent(final UserTO item, final AjaxRequestTarget target) {
423 this.item = item;
424 this.target = target;
425 }
426
427 public UserTO getItem() {
428 return item;
429 }
430
431 public AjaxRequestTarget getTarget() {
432 return target;
433 }
434 }
435 }