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.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                         // reduce number of method calls by building a lookup table
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 }