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.console.panels;
20  
21  import java.io.Serializable;
22  import java.lang.reflect.Field;
23  import java.lang.reflect.InvocationTargetException;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.List;
27  import java.util.Optional;
28  import org.apache.commons.lang3.StringUtils;
29  import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
30  import org.apache.syncope.client.console.wicket.markup.html.form.ActionLinksTogglePanel;
31  import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
32  import org.apache.syncope.client.console.wizards.WizardMgtPanel;
33  import org.apache.syncope.client.ui.commons.Constants;
34  import org.apache.syncope.client.ui.commons.ajax.form.IndicatorAjaxFormChoiceComponentUpdatingBehavior;
35  import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
36  import org.apache.wicket.AttributeModifier;
37  import org.apache.wicket.Component;
38  import org.apache.wicket.PageReference;
39  import org.apache.wicket.ajax.AjaxEventBehavior;
40  import org.apache.wicket.ajax.AjaxRequestTarget;
41  import org.apache.wicket.core.util.lang.PropertyResolver;
42  import org.apache.wicket.event.IEvent;
43  import org.apache.wicket.markup.html.WebMarkupContainer;
44  import org.apache.wicket.markup.html.basic.Label;
45  import org.apache.wicket.markup.html.form.Check;
46  import org.apache.wicket.markup.html.form.CheckGroup;
47  import org.apache.wicket.markup.html.form.CheckGroupSelector;
48  import org.apache.wicket.markup.html.list.ListItem;
49  import org.apache.wicket.markup.html.list.ListView;
50  import org.apache.wicket.model.IModel;
51  import org.apache.wicket.model.Model;
52  import org.apache.wicket.model.ResourceModel;
53  import org.apache.wicket.request.cycle.RequestCycle;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  public abstract class ListViewPanel<T extends Serializable> extends WizardMgtPanel<T> {
58  
59      private static final long serialVersionUID = -7982691107029848579L;
60  
61      private static final Logger LOG = LoggerFactory.getLogger(ListViewPanel.class);
62  
63      private ActionLinksTogglePanel<T> togglePanel;
64  
65      public enum CheckAvailability {
66  
67          /**
68           * No checks.
69           */
70          NONE,
71          /**
72           * Enabled checks including check group selector.
73           */
74          AVAILABLE,
75          /**
76           * Disabled checks.
77           */
78          DISABLED
79  
80      }
81  
82      private final CheckGroupSelector groupSelector;
83  
84      private final Model<CheckAvailability> check;
85  
86      private final ListView<T> beans;
87  
88      private final List<T> listOfItems;
89  
90      /**
91       * Table view of a list of beans.
92       *
93       * @param id id.
94       * @param list list of item.
95       * @param reference list item reference class.
96       * @param includes Used to sort and restrict the set of bean's fields to be shown.
97       * @param actions item actions.
98       */
99      private ListViewPanel(
100             final String id,
101             final List<T> list,
102             final Class<T> reference,
103             final List<String> includes,
104             final ActionsPanel<T> actions,
105             final CheckAvailability check,
106             final boolean reuseItem,
107             final boolean wizardInModal,
108             final boolean captionVisible,
109             final IModel<? extends Collection<T>> model) {
110 
111         super(id, wizardInModal);
112         setOutputMarkupId(true);
113 
114         togglePanel = getTogglePanel();
115 
116         this.check = Model.of(check);
117 
118         WebMarkupContainer captionContainer = new WebMarkupContainer("captionContainer");
119         captionContainer.setOutputMarkupPlaceholderTag(true);
120         captionContainer.setVisible(captionVisible);
121         addInnerObject(captionContainer);
122 
123         Label caption = new Label("caption", new ResourceModel("listview.caption", StringUtils.EMPTY));
124         captionContainer.add(caption);
125 
126         CheckGroup<T> checkGroup = new CheckGroup<>("group", model);
127         checkGroup.setOutputMarkupId(true);
128         checkGroup.add(new IndicatorAjaxFormChoiceComponentUpdatingBehavior() {
129 
130             private static final long serialVersionUID = -151291731388673682L;
131 
132             @Override
133             protected void onUpdate(final AjaxRequestTarget target) {
134                 // ignore
135             }
136         });
137         addInnerObject(checkGroup);
138 
139         groupSelector = new CheckGroupSelector("groupselector", checkGroup);
140         addInnerObject(groupSelector.setOutputMarkupId(true).
141                 setOutputMarkupPlaceholderTag(true).
142                 setVisible(this.check.getObject() == CheckAvailability.AVAILABLE).
143                 setEnabled(this.check.getObject() == CheckAvailability.AVAILABLE));
144 
145         final List<String> toBeIncluded;
146         if (includes == null || includes.isEmpty()) {
147             toBeIncluded = new ArrayList<>();
148             for (Field field : reference.getDeclaredFields()) {
149                 toBeIncluded.add(field.getName());
150             }
151         } else {
152             toBeIncluded = includes;
153         }
154 
155         if (toBeIncluded.isEmpty()) {
156             LOG.warn("No field has been retrieved from {}", reference.getName());
157             listOfItems = new ArrayList<>();
158         } else if (list == null || list.isEmpty()) {
159             LOG.info("No item to be shown");
160             listOfItems = new ArrayList<>();
161         } else {
162             listOfItems = list;
163             LOG.debug("Show fields {}", toBeIncluded);
164         }
165 
166         addInnerObject(header(toBeIncluded));
167 
168         beans = new ListView<>("beans", listOfItems) {
169 
170             private static final long serialVersionUID = -9112553137618363167L;
171 
172             @Override
173             protected void populateItem(final ListItem<T> beanItem) {
174                 beanItem.add(new Check<>("check", beanItem.getModel(), checkGroup).setOutputMarkupId(true).
175                         setOutputMarkupPlaceholderTag(true).
176                         setVisible(ListViewPanel.this.check.getObject() == CheckAvailability.AVAILABLE
177                                 || ListViewPanel.this.check.getObject() == CheckAvailability.DISABLED).
178                         setEnabled(ListViewPanel.this.check.getObject() == CheckAvailability.AVAILABLE));
179 
180                 final T bean = beanItem.getModelObject();
181 
182                 final ListView<String> fields = new ListView<>("fields", toBeIncluded) {
183 
184                     private static final long serialVersionUID = -9112553137618363167L;
185 
186                     @Override
187                     protected void populateItem(final ListItem<String> fieldItem) {
188                         fieldItem.add(getValueComponent(fieldItem.getModelObject(), bean));
189                         if (togglePanel != null) {
190                             fieldItem.add(new AttributeModifier("style", "cursor: pointer;"));
191                             fieldItem.add(new AjaxEventBehavior(Constants.ON_CLICK) {
192 
193                                 private static final long serialVersionUID = -9027652037484739586L;
194 
195                                 @Override
196                                 protected String findIndicatorId() {
197                                     return StringUtils.EMPTY;
198                                 }
199 
200                                 @Override
201                                 protected void onEvent(final AjaxRequestTarget target) {
202                                     togglePanel.toggleWithContent(
203                                             target,
204                                             actions.cloneWithLabels("actions", new Model<>(bean)),
205                                             bean);
206                                 }
207                             });
208                         }
209                     }
210                 };
211 
212                 beanItem.add(fields);
213 
214                 if (togglePanel == null) {
215                     beanItem.add(actions.clone("actions", new Model<>(bean)));
216                 } else {
217                     beanItem.add(new ActionsPanel<>("actions", new Model<>(bean)).setVisible(false).setEnabled(false));
218                 }
219             }
220         };
221         beans.setOutputMarkupId(true);
222         beans.setReuseItems(reuseItem);
223         beans.setRenderBodyOnly(true);
224         checkGroup.add(beans);
225     }
226 
227     protected ListView<String> header(final List<String> labels) {
228         return new ListView<>("names", labels) {
229 
230             private static final long serialVersionUID = -9112553137618363167L;
231 
232             @Override
233             protected void populateItem(final ListItem<String> item) {
234                 item.add(new Label(Constants.NAME_FIELD_NAME,
235                         new ResourceModel(item.getModelObject(), item.getModelObject())));
236             }
237         };
238     }
239 
240     /**
241      * Use this to refresh the ListView with updated items (e.g. from callback methods)
242      *
243      * @param elements new items
244      */
245     public void refreshList(final List<T> elements) {
246         beans.setList(elements);
247     }
248 
249     public void setCheckAvailability(final CheckAvailability check) {
250         // used to perform selectable enabling check condition
251         this.check.setObject(check);
252 
253         RequestCycle.get().find(AjaxRequestTarget.class).ifPresent(t -> {
254             // reload group selector
255             t.add(groupSelector.setVisible(check == CheckAvailability.AVAILABLE), groupSelector.getMarkupId());
256             // reload the list view panel
257             t.add(ListViewPanel.this, getMarkupId());
258         });
259     }
260 
261     protected abstract Component getValueComponent(String key, T bean);
262 
263     /**
264      * ListViewPanel builder.
265      *
266      * @param <T> list item reference type.
267      */
268     public static class Builder<T extends Serializable> extends WizardMgtPanel.Builder<T> {
269 
270         private static final long serialVersionUID = -3643771352897992172L;
271 
272         private IModel<? extends Collection<T>> model = Model.of(List.of());
273 
274         private final List<String> includes = new ArrayList<>();
275 
276         private final ActionsPanel<T> actions;
277 
278         private List<T> items;
279 
280         private CheckAvailability check = CheckAvailability.NONE;
281 
282         private boolean reuseItem = true;
283 
284         private boolean captionVisible = true;
285 
286         private final Class<T> reference;
287 
288         public Builder(final Class<T> reference, final PageReference pageRef) {
289             super(pageRef);
290             this.reference = reference;
291             this.items = null;
292             this.actions = new ActionsPanel<>("actions", null);
293         }
294 
295         public Builder<T> setModel(final IModel<? extends Collection<T>> model) {
296             this.model = model;
297             return this;
298         }
299 
300         /**
301          * Sets list of items.
302          *
303          * @param items list of items.
304          * @return current builder object.
305          */
306         public Builder<T> setItems(final List<T> items) {
307             this.items = items;
308             return this;
309         }
310 
311         /**
312          * Adds item.
313          *
314          * @param item item.
315          * @return current builder object.
316          */
317         public Builder<T> addItem(final T item) {
318             if (item == null) {
319                 return this;
320             }
321 
322             if (this.items == null) {
323                 this.items = new ArrayList<>();
324             }
325 
326             this.items.add(item);
327             return this;
328         }
329 
330         public Builder<T> withChecks(final CheckAvailability check) {
331             this.check = check;
332             return this;
333         }
334 
335         public Builder<T> setReuseItem(final boolean reuseItem) {
336             this.reuseItem = reuseItem;
337             return this;
338         }
339 
340         public Builder<T> setCaptionVisible(final boolean captionVisible) {
341             this.captionVisible = captionVisible;
342             return this;
343         }
344 
345         /**
346          * Gives fields to be shown. It could be used to give an order as well.
347          *
348          * @param includes field names to be shown.
349          * @return current builder object.
350          */
351         public Builder<T> includes(final String... includes) {
352             for (String include : includes) {
353                 if (include != null && !this.includes.contains(include)) {
354                     this.includes.add(include);
355                 }
356             }
357             return this;
358         }
359 
360         /**
361          * Add item action (the given order is ignored.
362          *
363          * @param link action link.
364          * @param type action type.
365          * @param entitlements entitlements.
366          * @return current builder object.
367          */
368         public Builder<T> addAction(
369                 final ActionLink<T> link, final ActionLink.ActionType type, final String entitlements) {
370             return addAction(link, type, entitlements, false);
371         }
372 
373         /**
374          * Add item action (the given order is ignored.
375          *
376          * @param link action link.
377          * @param type action type.
378          * @param entitlements entitlements.
379          * @param onConfirm specify TRUE to ask for confirmation.
380          * @return current builder object.
381          */
382         public Builder<T> addAction(
383                 final ActionLink<T> link,
384                 final ActionLink.ActionType type,
385                 final String entitlements,
386                 final boolean onConfirm) {
387             actions.add(link, type, entitlements, onConfirm).hideLabel();
388             return this;
389         }
390 
391         /**
392          * Overridable method to generate field value rendering component.
393          *
394          * @param key field key.
395          * @param bean source bean.
396          * @return field rendering component.
397          */
398         protected Component getValueComponent(final String key, final T bean) {
399             LOG.debug("Processing field {}", key);
400 
401             Object value;
402             try {
403                 value = PropertyResolver.getPropertyGetter(key, bean).invoke(bean);
404             } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
405                 LOG.error("Error retrieving value for field {}", key, e);
406                 value = StringUtils.EMPTY;
407             }
408 
409             LOG.debug("Field value {}", value);
410 
411             return Optional.ofNullable(value)
412                     .map(o -> new Label("field", new ResourceModel(o.toString(), o.toString())))
413                     .orElseGet(() -> new Label("field", StringUtils.EMPTY));
414         }
415 
416         protected T getActualItem(final T item, final List<T> list) {
417             return item == null
418                     ? null
419                     : list.stream().filter(item::equals).findAny().orElse(null);
420         }
421 
422         @Override
423         protected WizardMgtPanel<T> newInstance(final String id, final boolean wizardInModal) {
424             return new ListViewPanel<>(
425                     id, items, reference, includes, actions, check, reuseItem, wizardInModal, captionVisible, model) {
426 
427                 private static final long serialVersionUID = -1715389337530657988L;
428 
429                 @Override
430                 protected Component getValueComponent(final String key, final T bean) {
431                     return Builder.this.getValueComponent(key, bean);
432                 }
433 
434                 @Override
435                 protected T getActualItem(final T item, final List<T> list) {
436                     return Builder.this.getActualItem(item, list);
437                 }
438 
439                 @Override
440                 protected void customActionCallback(final AjaxRequestTarget target) {
441                     Builder.this.customActionCallback(target);
442                 }
443 
444                 @Override
445                 protected void customActionOnFinishCallback(final AjaxRequestTarget target) {
446                     Builder.this.customActionOnFinishCallback(target);
447                 }
448 
449                 @Override
450                 protected void customActionOnCancelCallback(final AjaxRequestTarget target) {
451                     Builder.this.customActionOnCancelCallback(target);
452                 }
453 
454                 @Override
455                 protected ActionLinksTogglePanel<T> getTogglePanel() {
456                     return Builder.this.getTogglePanel();
457                 }
458             };
459         }
460 
461         protected ActionLinksTogglePanel<T> getTogglePanel() {
462             return null;
463         }
464 
465         protected void customActionCallback(final AjaxRequestTarget target) {
466         }
467 
468         protected void customActionOnCancelCallback(final AjaxRequestTarget target) {
469         }
470 
471         protected void customActionOnFinishCallback(final AjaxRequestTarget target) {
472         }
473     }
474 
475     @Override
476     @SuppressWarnings("unchecked")
477     public void onEvent(final IEvent<?> event) {
478         if (event.getPayload() instanceof AjaxWizard.NewItemEvent) {
479             final T item = ((AjaxWizard.NewItemEvent<T>) event.getPayload()).getItem();
480             final Optional<AjaxRequestTarget> target = ((AjaxWizard.NewItemEvent<T>) event.getPayload()).getTarget();
481 
482             if (event.getPayload() instanceof AjaxWizard.NewItemFinishEvent) {
483                 final T old = getActualItem(item, ListViewPanel.this.listOfItems);
484                 int indexOf = ListViewPanel.this.listOfItems.size();
485                 if (old != null) {
486                     indexOf = ListViewPanel.this.listOfItems.indexOf(old);
487                     ListViewPanel.this.listOfItems.remove(old);
488                 }
489                 ListViewPanel.this.listOfItems.add(indexOf, item);
490             }
491 
492             target.ifPresent(t -> t.add(ListViewPanel.this));
493             super.onEvent(event);
494         } else if (event.getPayload() instanceof ListViewPanel.ListViewReload) {
495             final ListViewPanel.ListViewReload<?> payload = (ListViewPanel.ListViewReload<?>) event.getPayload();
496             if (payload.getItems() != null) {
497                 ListViewPanel.this.listOfItems.clear();
498                 try {
499                     ListViewPanel.this.listOfItems.addAll((List<T>) payload.getItems());
500                 } catch (RuntimeException re) {
501                     LOG.warn("Error reloading items", re);
502                 }
503             }
504             payload.getTarget().add(ListViewPanel.this);
505         } else {
506             super.onEvent(event);
507         }
508     }
509 
510     protected abstract T getActualItem(T item, List<T> list);
511 
512     public static class ListViewReload<T extends Serializable> implements Serializable {
513 
514         private static final long serialVersionUID = 1509151005816590312L;
515 
516         private final AjaxRequestTarget target;
517 
518         private final List<T> items;
519 
520         public ListViewReload(final AjaxRequestTarget target) {
521             this.target = target;
522             this.items = null;
523         }
524 
525         public ListViewReload(final List<T> items, final AjaxRequestTarget target) {
526             this.target = target;
527             this.items = items;
528         }
529 
530         public AjaxRequestTarget getTarget() {
531             return target;
532         }
533 
534         public List<T> getItems() {
535             return items;
536         }
537     }
538 
539     protected ActionLinksTogglePanel<T> getTogglePanel() {
540         return null;
541     }
542 }