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