View Javadoc
1   /**
2    * This file Copyright (c) 2013-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.security.app.dialog.field;
35  
36  import info.magnolia.icons.MagnoliaIcons;
37  
38  import java.util.ArrayList;
39  import java.util.List;
40  import java.util.Map;
41  
42  import com.vaadin.ui.Button;
43  import com.vaadin.ui.Component;
44  import com.vaadin.v7.data.Property;
45  import com.vaadin.v7.data.Validator;
46  import com.vaadin.v7.data.util.ObjectProperty;
47  import com.vaadin.v7.ui.AbstractField;
48  import com.vaadin.v7.ui.CustomField;
49  import com.vaadin.v7.ui.Field;
50  import com.vaadin.v7.ui.HorizontalLayout;
51  import com.vaadin.v7.ui.Label;
52  import com.vaadin.v7.ui.VerticalLayout;
53  
54  /**
55   * A field implementation dedicated to access control lists. In effect, this is a multi-value field
56   * for {@link AccessControlField AccessControlFields}, supporting addition and removal of entries.
57   *
58   * <p>New entries are created through the </p>
59   */
60  public class AccessControlListField extends CustomField<AccessControlList> {
61      private final Map<Long, String> permissions;
62      private final NewEntryHandler newEntryHandler;
63  
64      private VerticalLayout layout;
65      private List<Field<AccessControlList.Entry>> entryFields = new ArrayList<>();
66      private ValueChangeListener valueChangeListener;
67  
68      private String addButtonCaption = "Add";
69      private String removeButtonCaption = "Remove";
70      private String emptyPlaceholderCaption = "No access";
71      private EntryFieldFactory entryFieldFactory;
72  
73      /**
74       * Creates an AccessControlListField with the given set of permissions.
75       *
76       * @param permissions a map whose keys are permission values, and whose values are the corresponding captions to display in the permission select.
77       * @param newEntryHandler the handler creating actual entries with default values, to append to this field.
78       */
79      public AccessControlListField(Map<Long, String> permissions, NewEntryHandler newEntryHandler) {
80          this.permissions = permissions;
81          this.newEntryHandler = newEntryHandler;
82      }
83  
84      @Override
85      protected Component initContent() {
86          layout = new VerticalLayout();
87          layout.setSpacing(false);
88          layout.addStyleName("access-control-list-field");
89  
90          AccessControlList<AccessControlList.Entry> acl = getValue();
91          List<Component> entryRows = buildEntryRows(acl);
92          layout.addComponents(entryRows.toArray(new Component[0]));
93  
94          if (acl.getEntries().isEmpty()) {
95              layout.addComponent(new Label(emptyPlaceholderCaption));
96          }
97  
98          if (newEntryHandler != null) {
99              final Button addButton = new Button(addButtonCaption);
100             addButton.addClickListener(new Button.ClickListener() {
101                 @Override
102                 public void buttonClick(Button.ClickEvent event) {
103                     AccessControlList.Entry entry = newEntryHandler.createEntry();
104                     AccessControlList<AccessControlList.Entry> value = getValue();
105                     if (value == null) {
106                         value = new AccessControlList<>();
107                         setValue(value);
108                     }
109                     value.addEntry(entry);
110 
111                     Field<AccessControlList.Entry> entryField = getEntryFieldFactory().createField(entry);
112                     // set validation visibility from parent field
113                     if (entryField instanceof AbstractField) {
114                         ((AbstractField) entryField).setValidationVisible(isValidationVisible());
115                     }
116 
117                     // update layout
118                     entryFields.add(entryField);
119                     layout.addComponent(buildEntryRow(entryField), layout.getComponentCount() - 1);
120                     if (layout.getComponent(0) instanceof Label) {
121                         layout.removeComponent(layout.getComponent(0));
122                     }
123                 }
124             });
125             layout.addComponent(addButton);
126         }
127         return layout;
128     }
129 
130     protected List<Component> buildEntryRows(AccessControlList<AccessControlList.Entry> acl) {
131         List<Component> entryRows = new ArrayList<>();
132         if (acl != null) {
133             for (final AccessControlList.Entry entry : acl.getEntries()) {
134                 Field<AccessControlList.Entry> entryField = getEntryFieldFactory().createField(entry);
135                 // set validation visibility from parent field
136                 if (entryField instanceof AbstractField) {
137                     ((AbstractField) entryField).setValidationVisible(isValidationVisible());
138                 }
139 
140                 entryFields.add(entryField);
141                 entryRows.add(buildEntryRow(entryField));
142             }
143         }
144         return entryRows;
145     }
146 
147     protected Component buildEntryRow(final Field<AccessControlList.Entry> entryField) {
148         final HorizontalLayout entryRow = new HorizontalLayout();
149         entryRow.setSpacing(true);
150         entryRow.setWidth("100%");
151 
152         Button deleteButton = new Button(MagnoliaIcons.TRASH);
153         deleteButton.addStyleName("v-button-icon icon-button");
154         deleteButton.setDescription(removeButtonCaption);
155         deleteButton.addClickListener(new Button.ClickListener() {
156 
157             @Override
158             public void buttonClick(Button.ClickEvent event) {
159                 AccessControlList<AccessControlList.Entry> acl = getValue();
160                 acl.removeEntry(entryField.getValue());
161                 entryFields.remove(entryField);
162                 if (isValidationVisible()) {
163                     // make sure to re-validate if problematic entry is removed
164                     fireValueChange(false);
165                 }
166 
167                 // update layout
168                 layout.removeComponent(entryRow);
169                 if (acl.getEntries().size() == 0) {
170                     layout.addComponentAsFirst(new Label(emptyPlaceholderCaption));
171                 }
172             }
173         });
174 
175         entryRow.addComponents(entryField, deleteButton);
176         entryRow.setExpandRatio(entryField, 1f);
177         return entryRow;
178     }
179 
180     public String getAddButtonCaption() {
181         return addButtonCaption;
182     }
183 
184     public void setAddButtonCaption(String addButtonCaption) {
185         this.addButtonCaption = addButtonCaption;
186     }
187 
188     public String getRemoveButtonCaption() {
189         return removeButtonCaption;
190     }
191 
192     public void setRemoveButtonCaption(String removeButtonCaption) {
193         this.removeButtonCaption = removeButtonCaption;
194     }
195 
196     public String getEmptyPlaceholderCaption() {
197         return emptyPlaceholderCaption;
198     }
199 
200     public void setEmptyPlaceholderCaption(String emptyPlaceholderCaption) {
201         this.emptyPlaceholderCaption = emptyPlaceholderCaption;
202     }
203 
204     @Override
205     public Class<AccessControlList> getType() {
206         return AccessControlList.class;
207     }
208 
209     @Override
210     public AccessControlList<AccessControlList.Entry> getValue() {
211         return super.getValue();
212     }
213 
214     @Override
215     protected void validate(AccessControlList fieldValue) throws Validator.InvalidValueException {
216         List<Validator.InvalidValueException> causes = new ArrayList<>();
217         // first invoke self-validation
218         try {
219             super.validate(fieldValue);
220         } catch (Validator.InvalidValueException e) {
221             causes.add(e);
222         }
223 
224         // then validate individual entries
225         for (Field<AccessControlList.Entry> entryField : entryFields) {
226             try {
227                 entryField.validate();
228             } catch (Validator.InvalidValueException e) {
229                 causes.add(e);
230             }
231         }
232 
233         if (!causes.isEmpty()) {
234             // until we display errors on sub-fields, display causes' messages by passing null in first arg
235             // see AbstractErrorMessage#getFormattedHtmlMessage
236             throw new Validator.InvalidValueException(null, causes.toArray(new Validator.InvalidValueException[0]));
237         }
238     }
239 
240     /*
241      * Make sure to propagate inner value changes when validation is visible (i.e. field is invalid);
242      * so that corrected values are validated on the fly and eventually remove validation marks.
243      */
244     @Override
245     public void setValidationVisible(boolean validateAutomatically) {
246         super.setValidationVisible(validateAutomatically);
247         for (Field<?> entryField : entryFields) {
248             if (entryField instanceof AbstractField) {
249                 ((AbstractField) entryField).setValidationVisible(isValidationVisible());
250             }
251         }
252 
253         if (validateAutomatically && valueChangeListener == null) {
254             valueChangeListener = new ValueChangeListener() {
255                 @Override
256                 public void valueChange(Property.ValueChangeEvent event) {
257                     fireValueChange(false);
258                 }
259             };
260             for (Field<?> entryField : entryFields) {
261                 entryField.addValueChangeListener(valueChangeListener);
262             }
263         } else if (!validateAutomatically && valueChangeListener != null) {
264             for (Field<?> entryField : entryFields) {
265                 entryField.removeValueChangeListener(valueChangeListener);
266             }
267             valueChangeListener = null;
268         }
269     }
270 
271     public EntryFieldFactory getEntryFieldFactory() {
272         if (entryFieldFactory == null) {
273             this.entryFieldFactory = new DefaultEntryFieldFactory();
274         }
275         return entryFieldFactory;
276     }
277 
278     public void setEntryFieldFactory(EntryFieldFactory entryFieldFactory) {
279         this.entryFieldFactory = entryFieldFactory;
280     }
281 
282     /**
283      * The handler used to create new ACL entries.
284      */
285     public interface NewEntryHandler {
286         /**
287          * Creates and returns a new {@link AccessControlList.Entry Entry} to feed into this field's {@linkplain AccessControlList acl} value.
288          */
289         AccessControlList.Entry createEntry();
290     }
291 
292     /**
293      * A factory creating individual fields for ACL entries.
294      */
295     public interface EntryFieldFactory {
296         /**
297          * Creates a {@link Field} and binds the given {@link AccessControlList.Entry Entry} to it.
298          */
299         Field<AccessControlList.Entry> createField(AccessControlList.Entry entry);
300     }
301 
302     /**
303      * Default implementation creates a standard AccessControlField with validator.
304      */
305     public class DefaultEntryFieldFactory implements EntryFieldFactory {
306         @Override
307         public Field<AccessControlList.Entry> createField(AccessControlList.Entry entry) {
308             AccessControlFieldd/AccessControlField.html#AccessControlField">AccessControlField entryField = new AccessControlField(permissions);
309             entryField.setPropertyDataSource(new ObjectProperty<>(entry));
310             return entryField;
311         }
312     }
313 }