View Javadoc
1   /**
2    * This file Copyright (c) 2015-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.dialog.formdialog;
35  
36  import info.magnolia.objectfactory.Components;
37  import info.magnolia.ui.api.app.SubAppContext;
38  import info.magnolia.ui.api.context.UiContext;
39  import info.magnolia.ui.api.i18n.I18NAuthoringSupport;
40  import info.magnolia.ui.form.Form;
41  import info.magnolia.ui.form.FormItem;
42  import info.magnolia.ui.form.FormPresenter;
43  import info.magnolia.ui.form.FormTab;
44  import info.magnolia.ui.form.definition.FormDefinition;
45  import info.magnolia.ui.form.definition.TabDefinition;
46  import info.magnolia.ui.form.field.definition.FieldDefinition;
47  import info.magnolia.ui.vaadin.form.FormSection;
48  import info.magnolia.ui.vaadin.form.FormViewReduced;
49  
50  import java.util.Iterator;
51  import java.util.List;
52  import java.util.Locale;
53  import java.util.Map;
54  import java.util.Optional;
55  
56  import javax.inject.Inject;
57  
58  import org.apache.commons.lang3.ObjectUtils;
59  import org.apache.commons.lang3.StringUtils;
60  
61  import com.google.common.collect.Maps;
62  import com.vaadin.ui.Component;
63  import com.vaadin.v7.data.Item;
64  import com.vaadin.v7.ui.Field;
65  
66  /**
67   * Default implementation of {@link info.magnolia.ui.form.FormPresenter}. Main responsibilities of the class are:
68   * <ul>
69   * <li>Communication with {@link FormBuilder}</li>
70   * <li>Tracking locale-specific representations of {@link FormSection}s</li>
71   * <li>Listening to the {@link FormView}'s locale switcher and setting {@link SubAppContext}-wide locale.</li>
72   * </ul>
73   *
74   * <strong>NOTE:</strong> ideally this class should have reside at the {@code info.magnolia.ui.form} package in <strong>magnolia-ui-form</strong>,
75   * but due to {@link FormBuilder} being in <strong>magnolia-ui-dialog</strong> and the incorrect relation between {@link FormView}, {@link info.magnolia.ui.dialog.DialogView DialogView} and
76   * {@link FormViewReduced} it has to in the current package.
77   */
78  public class FormPresenterImpl implements FormPresenter {
79  
80      private final Map<String, Field<?>> cachedFields = Maps.newHashMap();
81  
82      private final Map<String, FormTab> cachedTabs = Maps.newHashMap();
83  
84      private final I18NAuthoringSupport i18nAuthoringSupport;
85  
86      private final FormBuilder formBuilder;
87  
88      private final UiContext uiContext;
89  
90      private FormDefinition formDefinition;
91  
92      private FormViewReduced formView;
93  
94      private Locale activeLocale;
95  
96      private Item itemDatasource;
97  
98      @Inject
99      public FormPresenterImpl(FormBuilder formBuilder, UiContext uiContext, I18NAuthoringSupport i18nAuthoringSupport) {
100         this.i18nAuthoringSupport = i18nAuthoringSupport;
101         this.formBuilder = formBuilder;
102         this.uiContext = uiContext;
103     }
104 
105     /**
106      * @deprecated since 5.4.12 - use {@link #FormPresenterImpl(FormBuilder, UiContext, I18NAuthoringSupport)} instead.
107      */
108     @Deprecated
109     public FormPresenterImpl(FormBuilder formBuilder, UiContext uiContext) {
110         this(formBuilder, uiContext, Components.getComponent(I18NAuthoringSupport.class));
111     }
112 
113     @Override
114     public void presentView(FormViewReduced formView, FormDefinition formDefinition, Item item, FormItem parent) {
115         this.formView = formView;
116         this.itemDatasource = item;
117         this.formDefinition = formDefinition;
118 
119         cachedFields.clear();
120         cachedTabs.clear();
121         activeLocale = getCurrentAuthoringLocale(uiContext, item);
122         // FormBuilder still expects the FormView object to build, so we have to cast here but ideally that should be refactored
123         buildForm((FormView) this.formView, formDefinition, item, parent, activeLocale);
124 
125         // We should expand locale-awareness onto all the UI contexts.
126         if (uiContext instanceof SubAppContext) {
127             formView.setListener(newLocale -> {
128                 if (newLocale != null && !ObjectUtils.equals(((SubAppContext) uiContext).getAuthoringLocale(), newLocale)) {
129                     setLocale(newLocale);
130                 }
131             });
132         }
133     }
134 
135     @Override
136     public boolean isValid() {
137         return formView.isValid();
138     }
139 
140     @Override
141     public void setLocale(Locale locale) {
142         if (uiContext instanceof SubAppContext && !ObjectUtils.equals(locale, this.activeLocale)) {
143             this.activeLocale = locale;
144             ((SubAppContext) uiContext).setAuthoringLocale(locale);
145             updateForm(locale);
146         }
147     }
148 
149     protected void updateForm(Locale locale) {
150         for (FormSection formSection : formView.getFormSections()) {
151             String tabName = formSection.getName();
152             Optional<TabDefinition> matchingTab = formDefinition.getTabs().stream()
153                     .filter(tabDefinition -> tabDefinition.getName().equals(tabName))
154                     .findFirst();
155 
156             if (matchingTab.isPresent()) {
157                 TabDefinition tabDefinition = matchingTab.get();
158                 FormTab formTab = cachedTabs.get(tabDefinition.getName());
159                 formSection.removeAllComponents();
160                 for (FieldDefinition fieldDefinition : tabDefinition.getFields()) {
161                     Field<?> field = getCachedField(fieldDefinition, locale);
162                     if (field == null) {
163                         field = createField(fieldDefinition, itemDatasource, formTab, locale);
164                         putToCache(fieldDefinition, field, locale);
165                     }
166                     if (field != null) {
167                         formSection.addComponent(field);
168                     }
169                 }
170             }
171         }
172 
173         if (formView instanceof ResurfaceFormViewImpl) {
174             ((ResurfaceFormViewImpl) formView).updateFormView();
175         }
176     }
177 
178     private void buildForm(FormView view, FormDefinition formDefinition, Item item, FormItem parent, Locale locale) {
179         final String description = formDefinition.getDescription();
180         final String label = formDefinition.getLabel();
181 
182         // If we remove the if blocks below, we show up the (first) generated key for this label/description (unless it is translated),
183         // thus overriding the dialog's title. See MGNLUI-2207.
184         // The 'container' of the form (ie a dialog) may already have set these values on the view based on its definition (dialogDefinition).
185         // Only if form specifies values - then use forms values.
186         if (StringUtils.isNotBlank(description) && !formBuilder.isMessageKey(description)) {
187             view.setDescription(description);
188         }
189 
190         if (StringUtils.isNotBlank(label) && !formBuilder.isMessageKey(label)) {
191             view.setCaption(label);
192         }
193 
194         buildReducedForm(formDefinition, view, item, parent, locale);
195 
196         if (formBuilder.hasI18nAwareFields(formDefinition)) {
197             view.setAvailableLocales(i18nAuthoringSupport.getAvailableLocales(item));
198             view.setCurrentLocale(locale);
199         }
200     }
201 
202     private void buildReducedForm(FormDefinition formDefinition, FormViewReduced view, Item item, FormItem parent, Locale locale) {
203         final Form form = new Form(formDefinition);
204         form.setParent(parent);
205         view.setItemDataSource(item);
206 
207         boolean firstFieldIsBuilt = false;
208 
209         for (TabDefinition tabDefinition : formDefinition.getTabs()) {
210             FormTab tab = buildFormTab(tabDefinition, item, form, locale);
211             if (tab == null) continue;
212 
213             Iterator<Component> fieldIt = tab.getContainer().iterator();
214             while (fieldIt.hasNext()) {
215                 view.addField((Field<?>) fieldIt.next());
216             }
217 
218             fieldIt = tab.getContainer().iterator();
219             if (!firstFieldIsBuilt && fieldIt.hasNext()) {
220                 ((Component.Focusable) fieldIt.next()).focus();
221                 firstFieldIsBuilt = true;
222             }
223 
224             view.addFormSection(tabDefinition.getLabel(), tab.getContainer());
225             cachedTabs.put(tabDefinition.getName(), tab);
226         }
227         view.setShowAllEnabled(view.getFormSections().size() > 1);
228     }
229 
230     private FormTab buildFormTab(TabDefinition tabDefinition, Item itemDatasource, Form parentForm, Locale locale) {
231         List<FieldDefinition> fields = tabDefinition.getFields();
232         if (fields == null || fields.isEmpty()) { // skip empty tabs
233             return null;
234         }
235         FormTab tab = new FormTab(tabDefinition);
236         tab.setParent(parentForm);
237         tab.getContainer().setName(tabDefinition.getName());
238         for (final FieldDefinition fieldDefinition : fields) {
239 
240             Field<?> field = getCachedField(fieldDefinition, locale);
241             if (field == null) {
242                 field = createField(fieldDefinition, itemDatasource, tab, locale);
243             }
244 
245             if (field != null) {
246                 tab.addField(field);
247                 final String helpDescription = fieldDefinition.getDescription();
248                 if (StringUtils.isNotBlank(helpDescription) && !formBuilder.isMessageKey(helpDescription)) {
249                     tab.setComponentHelpDescription(field, helpDescription);
250                 }
251             }
252         }
253         return tab;
254     }
255 
256     /**
257      * As of 5.3.9 only sub-app context supports tracking current authoring locale, we may expand that to other UiContexts in the future if needed.
258      */
259     private Locale getCurrentAuthoringLocale(UiContext uiContext, Item item) {
260         if (uiContext instanceof SubAppContext && ((SubAppContext) uiContext).getAuthoringLocale() != null) {
261             return ((SubAppContext) uiContext).getAuthoringLocale();
262         } else {
263             return i18nAuthoringSupport.getDefaultLocale(item);
264         }
265     }
266 
267     /**
268      * Creates field and put into the cache.
269      */
270     private Field<?> createField(FieldDefinition fieldDefinition, Item item, FormTab parentTab, Locale locale) {
271         Field<?> field = formBuilder.createField(fieldDefinition, item, parentTab);
272 
273         if (field != null) {
274             String cachedFieldName = getCachedFieldName(fieldDefinition, locale);
275             cachedFields.put(cachedFieldName, field);
276         }
277         return field;
278     }
279 
280     private String getCachedFieldName(FieldDefinition fieldDefinition, Locale locale) {
281         if (!formBuilder.isI18nAware(fieldDefinition) || locale == null) {
282             return fieldDefinition.getName();
283         }
284         String cachedFieldName = fieldDefinition.getName() + "_" + locale.getDisplayLanguage();
285         if (!locale.getDisplayCountry().isEmpty()) {
286             cachedFieldName += "_" + locale.getDisplayCountry();
287         }
288 
289         return cachedFieldName;
290     }
291 
292     private Field<?> getCachedField(FieldDefinition fieldDefinition, Locale locale) {
293         return cachedFields.get(getCachedFieldName(fieldDefinition, locale));
294     }
295 
296     private void putToCache(FieldDefinition fieldDefinition, Field<?> field, Locale locale) {
297         cachedFields.put(getCachedFieldName(fieldDefinition, locale), field);
298     }
299 }