View Javadoc
1   /**
2    * This file Copyright (c) 2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.contentapp.browser;
35  
36  import static java.util.stream.Collectors.toList;
37  
38  import java.lang.reflect.Field;
39  import java.util.Iterator;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.Set;
43  import java.util.function.BiPredicate;
44  import java.util.function.Predicate;
45  
46  import com.vaadin.data.Binder;
47  import com.vaadin.data.PropertySet;
48  import com.vaadin.data.provider.HierarchicalDataCommunicator;
49  import com.vaadin.data.provider.HierarchicalDataProvider;
50  import com.vaadin.data.provider.HierarchyMapper;
51  import com.vaadin.shared.Range;
52  import com.vaadin.ui.Grid;
53  import com.vaadin.ui.TreeGrid;
54  import com.vaadin.ui.components.grid.Editor;
55  import com.vaadin.ui.components.grid.EditorImpl;
56  
57  /**
58   * Magnolia-specific {@link TreeGrid} implementation.
59   * @param <T> item type.
60   */
61  public class MagnoliaTreeGrid<T> extends TreeGrid<T> {
62  
63      private final Predicate<T> itemInteractionAvailability;
64      private final BiPredicate<Column, T> cellEditingAvailability;
65  
66      public MagnoliaTreeGrid(PropertySet<T> propertySet, Predicate<T> itemInteractionAvailability, BiPredicate<Column, T> cellEditingAvailability) {
67          super(propertySet, new RemovedChildrenFilteringDataCommunicator<>(itemInteractionAvailability));
68          this.itemInteractionAvailability = itemInteractionAvailability;
69          this.cellEditingAvailability = cellEditingAvailability;
70          setPropertySet(propertySet);
71          
72          getEditor().addOpenListener(e -> {
73              if (!this.itemInteractionAvailability.test(e.getBean())) {
74                  getEditor().cancel();
75              }
76          });
77  
78          addSelectionListener(e -> e.getAllSelectedItems().forEach(item -> {
79              if (!itemInteractionAvailability.test(item)) {
80                  deselect(item);
81              }
82          }));
83      }
84  
85      @Override
86      public void select(T item) {
87          if (itemInteractionAvailability.test(item)) {
88              super.select(item);
89          }
90      }
91  
92      @Override
93      protected Editor<T> createEditor() {
94          return new EditableCellFilteringEditor<>(getPropertySet(), this, itemInteractionAvailability, cellEditingAvailability);
95      }
96  
97      /**
98       * Special implementation of {@link HierarchicalDataCommunicator} which
99       * will not refresh an item should it be not available according to the
100      * {@link ItemInteractionAvailability}, also facilitates a {@link RemovedChildrenFilteringHierarchyMapper}
101      * instead of a stock one.
102      *
103      * @param <T>
104      *     item type
105      */
106     static class RemovedChildrenFilteringDataCommunicator<T> extends HierarchicalDataCommunicator<T> {
107 
108         private final Predicate<T> itemInteractionAvailability;
109 
110         RemovedChildrenFilteringDataCommunicator(Predicate<T> itemInteractionAvailability) {
111             this.itemInteractionAvailability = itemInteractionAvailability;
112         }
113 
114         @Override
115         protected <F> HierarchyMapper<T, F> createHierarchyMapper(HierarchicalDataProvider<T, F> dataProvider) {
116             return new RemovedChildrenFilteringHierarchyMapper<>(dataProvider, itemInteractionAvailability);
117         }
118 
119         @Override
120         public void refresh(T data) {
121             if (itemInteractionAvailability.test(data)) {
122                 super.refresh(data);
123             }
124         }
125     }
126 
127     /**
128      * Hierarchy mapper implementation which will purge the removed
129      * child items from the internal books when the parent node is
130      * collapsed.
131      *
132      * @param <T>
133      *     item type
134      * @param <F>
135      *     filter type
136      */
137     static class RemovedChildrenFilteringHierarchyMapper<T, F> extends HierarchyMapper<T, F> {
138 
139         private final Predicate<T> itemInteractionAvailability;
140 
141         RemovedChildrenFilteringHierarchyMapper(HierarchicalDataProvider<T, F> provider, Predicate<T> itemInteractionAvailability) {
142             super(provider);
143             this.itemInteractionAvailability = itemInteractionAvailability;
144         }
145 
146         @Override
147         public Range collapse(T item, Integer position) {
148             int removedChildren;
149             if (itemInteractionAvailability.test(item)) {
150                 final Map<T, Set<T>> relationMap = accessChildMap();
151                 final Set<T> children = relationMap.get(item);
152 
153                 removedChildren = (int) children.stream().filter(child -> !itemInteractionAvailability.test(child)).count();
154             } else {
155                 getDataProvider().refreshAll();
156                 return Range.emptyRange();
157             }
158 
159             final Range toCollapse = super.collapse(item, position);
160             return Range.between(toCollapse.getStart(), toCollapse.getEnd() + removedChildren);
161         }
162 
163         @SuppressWarnings("unchecked")
164         Map<T, Set<T>> accessChildMap() {
165             try {
166                 final Field childMapField = HierarchyMapper.class.getDeclaredField("childMap");
167                 childMapField.setAccessible(true);
168                 return (Map<T, Set<T>>) childMapField.get(this);
169             } catch (NoSuchFieldException | IllegalAccessException e) {
170                 throw new RuntimeException(e);
171             }
172         }
173     }
174 
175     /**
176      * {@link EditorImpl} extension which will prevent certain cells from being
177      * edited (based on {@link ItemInteractionAvailability}) by manipulating the
178      * underlying {@link Binder} binding set.
179      *
180      * @param <T>
181      *     data type
182      */
183     private static class EditableCellFilteringEditor<T> extends EditorImpl<T> {
184 
185         private final Grid<T> grid;
186         private final Predicate<T> itemInteractionAvailability;
187         private final BiPredicate<Column, T> cellEditingAvailability;
188 
189         EditableCellFilteringEditor(PropertySet<T> propertySet, Grid<T> grid, Predicate<T> itemInteractionAvailability, BiPredicate<Column, T> cellEditingAvailability) {
190             super(propertySet);
191             this.grid = grid;
192             this.itemInteractionAvailability = itemInteractionAvailability;
193             this.cellEditingAvailability = cellEditingAvailability;
194             setBinder(new BindingFilterableBinder<>(propertySet));
195         }
196 
197         @Override
198         public BindingFilterableBinder<T> getBinder() {
199             return (BindingFilterableBinder<T>) super.getBinder();
200         }
201 
202         @Override
203         @SuppressWarnings("unchecked")
204         protected void doEdit(T bean) {
205             if (!itemInteractionAvailability.test(bean)) {
206                 cancel();
207                 return;
208             }
209 
210             // resolve all the columns that are editable in context of current item
211             final List<String> activeColumns = grid.getColumns().stream()
212                     .filter(Column::isEditable)
213                     .filter(column -> cellEditingAvailability.test(column, bean))
214                     .map(Column::getId)
215                     .collect(toList());
216 
217             // collect all the related bindings
218             final List<Binder.Binding> binding = activeColumns.stream()
219                     .map(grid::getColumn)
220                     .map(Column::getId)
221                     .map(id -> grid.getColumn(id).getEditorBinding())
222                     .collect(toList());
223 
224             // set the bindings to the binder
225             getBinder().setBindings((List) binding);
226 
227             super.doEdit(bean);
228 
229             // remove all the currently non-editable cell components from the editor state
230             retainOnlyActiveCellComponents(activeColumns);
231         }
232 
233         private void retainOnlyActiveCellComponents(List<String> activeColumns) {
234             final Iterator<Map.Entry<String, String>> componentMappings = getState().columnFields.entrySet().iterator();
235             final List<String> internalColumnIds = activeColumns.stream()
236                     .map(grid::getColumn)
237                     .map(this::getInternalIdForColumn)
238                     .collect(toList());
239 
240             while (componentMappings.hasNext()) {
241                 final Map.Entry<String, String> mapping = componentMappings.next();
242                 if (!internalColumnIds.contains(mapping.getKey())) {
243                     componentMappings.remove();
244                 }
245             }
246         }
247 
248         /**
249          * Simple {@link Binder} extension which allows to override
250          * the current binding set.
251          *
252          * @param <TYPE>
253          *     bound data type
254          */
255         static class BindingFilterableBinder<TYPE> extends Binder<TYPE> {
256 
257             private BindingFilterableBinder(PropertySet<TYPE> propertySet) {
258                 super(propertySet);
259             }
260 
261             public void setBindings(List<Binding<TYPE, ?>> bindings) {
262                 accessBindings().clear();
263                 accessBindings().addAll(bindings);
264             }
265 
266             @SuppressWarnings("unchecked")
267             List<Binding> accessBindings() {
268                 try {
269                     final Field childMapField = Binder.class.getDeclaredField("bindings");
270                     childMapField.setAccessible(true);
271                     return (List<Binding>) childMapField.get(this);
272                 } catch (NoSuchFieldException | IllegalAccessException e) {
273                     throw new RuntimeException(e);
274                 }
275             }
276         }
277 
278     }
279 }