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.framework.databinding.view;
35  
36  import static java.util.stream.Collectors.toList;
37  
38  import info.magnolia.ui.api.i18n.I18NAuthoringSupport;
39  import info.magnolia.ui.field.ComplexPropertyDefinition;
40  import info.magnolia.ui.field.factory.FieldBinder;
41  import info.magnolia.ui.field.factory.FieldFactory;
42  import info.magnolia.ui.form.FormDefinition;
43  import info.magnolia.ui.framework.databinding.ItemProviderStrategy;
44  import info.magnolia.ui.framework.ioc.UiContextBoundComponentProvider;
45  import info.magnolia.ui.framework.layout.LayoutDefinition;
46  import info.magnolia.ui.framework.layout.LayoutProducer;
47  import info.magnolia.ui.vaadin.form.field.FieldLayout;
48  
49  import java.util.Collections;
50  import java.util.LinkedHashMap;
51  import java.util.List;
52  import java.util.Locale;
53  import java.util.Map;
54  import java.util.Optional;
55  import java.util.stream.Stream;
56  
57  import javax.inject.Inject;
58  
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  import com.vaadin.data.Binder;
63  import com.vaadin.data.BinderValidationStatus;
64  import com.vaadin.data.HasValue;
65  import com.vaadin.data.ValidationException;
66  import com.vaadin.ui.Component;
67  
68  /**
69   * Builds a locale-aware form view. It first creates one {@link Binder} per {@link Locale},
70   * then binds form components accordingly and finally creates one layout for each available locale.
71   * @param <T> item type.
72   *
73   * @see FormPresenter
74   */
75  public class FormView<T> implements EditorView<T> {
76  
77      private static final Logger log = LoggerFactory.getLogger(FormView.class);
78  
79      private final Map<EditorView<?>, ItemProviderStrategy> subForms = new LinkedHashMap<>();
80      private final Map<Locale, Binder<T>> localisedBinders = new LinkedHashMap<>();
81      private final Map<Locale, Component> localisedLayouts = new LinkedHashMap<>();
82  
83      private final I18NAuthoringSupport i18NAuthoringSupport;
84  
85      @Inject
86      public FormView(FormDefinition<T> formDefinition,
87                      I18NAuthoringSupport i18NAuthoringSupport) {
88          this.i18NAuthoringSupport = i18NAuthoringSupport;
89          final FormPresenter<T> formPresenter = getComponentProvider().newInstance(FormPresenter.class);
90          List<Locale> availableLocales = i18NAuthoringSupport.getAvailableLocales();
91          if (availableLocales.isEmpty()) {
92              availableLocales = Collections.singletonList(i18NAuthoringSupport.getDefaultLocale());
93          }
94  
95          availableLocales
96                  .forEach(locale -> createLocaleSpecificBinderAndLayout(formDefinition, formPresenter, locale));
97      }
98  
99      @Override
100     public void populate(T item) {
101         localisedBinders.values().forEach(binder -> binder.readBean(item));
102         subForms.forEach(this::populateSubForm);
103     }
104 
105     /**
106      * Validates this forms and all its sub-forms recursively.
107      * @return a list of {@link BinderValidationStatus}
108      */
109     @Override
110     public List<BinderValidationStatus<?>> validate() {
111         final Stream<BinderValidationStatus<T>> fieldValidationResults = localisedBinders.values().stream().map(Binder::validate);
112         final Stream<BinderValidationStatus<?>> subFormValidationResults = subForms.keySet().stream()
113                 .flatMap(childForm -> childForm.validate().stream());
114 
115         return Stream.concat(fieldValidationResults, subFormValidationResults).collect(toList());
116     }
117 
118     @Override
119     public void write(T item) {
120         localisedBinders.values().forEach(binder -> safeWrite(binder, item));
121         subForms.forEach(this::writeSubForm);
122     }
123 
124     @Override
125     public Component asVaadinComponent() {
126         return getLayout(i18NAuthoringSupport.getDefaultLocale());
127     }
128 
129     @Override
130     public Component getLayout(Locale locale) {
131         return localisedLayouts.get(Optional.ofNullable(locale).orElse(i18NAuthoringSupport.getDefaultLocale()));
132     }
133 
134     private <FT> void populateSubForm(EditorView<FT> formView, ItemProviderStrategy<FT> itemProviderStrategy) {
135         itemProviderStrategy.read().ifPresent(formView::populate);
136     }
137 
138     private <FT> void writeSubForm(EditorView<FT> subForm, ItemProviderStrategy<FT> itemProviderStrategy) {
139         itemProviderStrategy.read().ifPresent(subForm::write);
140     }
141 
142     private void safeWrite(Binder<T> binder, T item) {
143         try {
144             if (binder.hasChanges()) {
145                 binder.writeBean(item);
146             }
147         } catch (ValidationException e) {
148             log.error("{}", e.getMessage(), e);
149         }
150     }
151 
152     private void createLocaleSpecificBinderAndLayout(FormDefinition<T> formDefinition, FormPresenter<T> formPresenter, Locale locale) {
153         Binder<T> binder = formPresenter.createBinder(formDefinition, locale);
154         localisedBinders.put(locale, binder);
155 
156         log.debug("Creating layout for form named '{}' and locale {}", formDefinition.getName(), locale);
157         final Map<String, Component> components = initialiseAndGetLocaleSpecificComponents(formDefinition, locale);
158         localisedLayouts.computeIfAbsent(locale, l -> doCreateLayout(formDefinition.getLayout(), components, binder));
159     }
160 
161     private Map<String, Component> initialiseAndGetLocaleSpecificComponents(FormDefinition<T> formDefinition, Locale locale) {
162         final Map<String, Component> localisedComponents = new LinkedHashMap<>();
163 
164         formDefinition.getFieldDefinitions().forEach(fieldDefinition -> {
165             final String key = fieldDefinition.getName();
166             log.debug("Creating component for {} and locale {}", key, locale);
167 
168             UiContextBoundComponentProvider fieldComponentProvider = getComponentProvider().inChildContext(fieldDefinition);
169 
170             FieldBinder<?> fieldBinder = (FieldBinder<?>) fieldComponentProvider.newInstance(fieldDefinition.getFieldBinderClass());
171             FieldFactory<?> fieldFactory = (FieldFactory<?>) fieldComponentProvider.newInstance(fieldDefinition.getFactoryClass());
172 
173             HasValue field = (HasValue) fieldFactory.createField();
174             Component component = (Component) field;
175             FieldLayout fieldLayout = FieldLayout.of(component, fieldDefinition.getDescription());
176 
177             Binder.BindingBuilder initialBinding = localisedBinders.get(locale)
178                     .forField(field)
179                     .withValidationStatusHandler(status -> fieldLayout.getValidationStatusHandler().accept(status.getMessage().orElse("")));
180 
181             fieldBinder
182                     .configureBinding(fieldDefinition, initialBinding)
183                     .bind(resolvePropertyNameByLocale(key, locale, fieldDefinition.isI18n()));
184 
185             String caption = component.getCaption();
186             if (locale != null && fieldDefinition.isI18n() && !i18NAuthoringSupport.isDefaultLocale(locale)) {
187                 caption = String.format("%s (%s)", caption, locale.toString());
188             }
189             fieldLayout.setCaption(caption);
190             fieldLayout.setRequiredIndicatorVisible(field.isRequiredIndicatorVisible());
191 
192             localisedComponents.put(key, fieldLayout);
193         });
194 
195         formDefinition.getSubFormDefinitions().forEach(def -> {
196             final String key = def.getName();
197             log.debug("Creating form component for {} and locale {}", key, locale);
198 
199             ComplexPropertyDefinition<?> asComplexPropertyDef = def;
200             ItemProviderStrategy<?> subFormProviderStrategy = this.create(asComplexPropertyDef.getItemProvider(), def);
201             EditorView<?> subForm = this.create(key, asComplexPropertyDef, subFormProviderStrategy, locale, asComplexPropertyDef);
202             Component component = subForm.getLayout(locale);
203             component.setCaption(def.getLabel());
204             subForms.putIfAbsent(subForm, subFormProviderStrategy);
205             localisedComponents.put(key, component);
206         });
207         return localisedComponents;
208     }
209 
210     private Component doCreateLayout(LayoutDefinition<?> definition, Map<String, Component> components, Binder<T> binder) {
211         final LayoutProducer<LayoutDefinition> producer = getComponentProvider().newInstance(definition.getImplementationClass(), binder);
212         final Component layout = producer.createLayout(definition, components);
213         layout.setSizeFull();
214         return layout;
215     }
216 
217     private String resolvePropertyNameByLocale(String name, Locale locale, boolean isI18nProperty) {
218         if (isI18nProperty && !i18NAuthoringSupport.isDefaultLocale(locale)) {
219             return i18NAuthoringSupport.deriveLocalisedPropertyName(name, locale);
220         }
221         return name;
222     }
223 }