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.framework.databinding.ItemProviderStrategy;
40  import info.magnolia.ui.field.ComplexPropertyDefinition;
41  import info.magnolia.ui.field.FieldDefinition;
42  import info.magnolia.ui.form.FormDefinition;
43  import info.magnolia.ui.field.factory.FormFieldFactory;
44  import info.magnolia.ui.framework.databinding.layout.LayoutDefinition;
45  import info.magnolia.ui.framework.databinding.layout.LayoutProducer;
46  
47  import java.util.Collections;
48  import java.util.HashMap;
49  import java.util.List;
50  import java.util.Locale;
51  import java.util.Map;
52  import java.util.Optional;
53  import java.util.stream.Stream;
54  
55  import javax.inject.Inject;
56  
57  import org.slf4j.Logger;
58  import org.slf4j.LoggerFactory;
59  
60  import com.vaadin.data.Binder;
61  import com.vaadin.data.BinderValidationStatus;
62  import com.vaadin.data.HasValue;
63  import com.vaadin.data.ValidationException;
64  import com.vaadin.ui.Component;
65  
66  /**
67   * Builds a locale-aware form view. It first creates one {@link Binder} per {@link Locale},
68   * then binds form components accordingly and finally creates one layout for each available locale.
69   * @param <T> item type.
70   *
71   * @see FormPresenter
72   */
73  public class FormView<T> implements EditorView<T> {
74  
75      private static final Logger log = LoggerFactory.getLogger(FormView.class);
76  
77      private final Map<EditorView<?>, ItemProviderStrategy> subForms = new HashMap<>();
78      private final Map<Locale, Binder<T>> localisedBinders = new HashMap<>();
79      private final Map<Locale, Component> localisedLayouts = new HashMap<>();
80  
81      private final I18NAuthoringSupport i18NAuthoringSupport;
82  
83      @Inject
84      public FormView(FormDefinition<T> formDefinition,
85                      FormFieldFactory formFieldFactory,
86                      I18NAuthoringSupport i18NAuthoringSupport) {
87  
88          this.i18NAuthoringSupport = i18NAuthoringSupport;
89          final FormPresenter<T> formPresenter = create(FormPresenter.class, this);
90          List<Locale> availableLocales = i18NAuthoringSupport.getAvailableLocales();
91          if (availableLocales.isEmpty()) {
92              availableLocales = Collections.singletonList(i18NAuthoringSupport.getDefaultLocale());
93          }
94  
95          availableLocales
96                  .forEach(locale -> createLocaleSpecificBinderAndLayout(formFieldFactory, 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(FormFieldFactory formFieldFactory, FormDefinition<T> formDefinition,
153                                                      FormPresenter<T> formPresenter, Locale locale) {
154         localisedBinders.put(locale, formPresenter.createBinder(formDefinition, locale));
155 
156         log.debug("Creating layout for form named '{}' and locale {}", formDefinition.getName(), locale);
157         final Map<String, Component> components = initialiseAndGetLocaleSpecificComponents(formDefinition, formFieldFactory, locale);
158         localisedLayouts.computeIfAbsent(locale, l -> doCreateLayout(formDefinition.getLayout(), components));
159     }
160 
161     private Map<String, Component> initialiseAndGetLocaleSpecificComponents(FormDefinition<T> formDefinition, FormFieldFactory formFieldFactory, Locale locale) {
162         final Map<String, Component> localisedComponents = new HashMap<>();
163 
164         formDefinition.getFieldDefinitions().forEach(fieldDefinition -> {
165             final String key = fieldDefinition.getName();
166             log.debug("Creating component for {} and locale {}", key, locale);
167 
168             final HasValue<Object> component = formFieldFactory.createField((FieldDefinition<Object>) fieldDefinition, locale, i18NAuthoringSupport);
169             localisedBinders.get(locale).bind(component, resolvePropertyNameByLocale(key, locale, fieldDefinition.isI18n()));
170             localisedComponents.put(key, (Component) component);
171         });
172 
173         formDefinition.getSubFormDefinitions().forEach(def -> {
174             final String key = def.getName();
175             log.debug("Creating form component for {} and locale {}", key, locale);
176 
177             final ItemProviderStrategy<?> subFormProviderStrategy = this.create(((ComplexPropertyDefinition<?>) def).getItemProvider(), def);
178             final EditorView<?> subForm = getViewProvider().create(def, subFormProviderStrategy, locale);
179             final Component component = subForm.getLayout(locale);
180             component.setCaption(def.getLabel());
181             subForms.putIfAbsent(subForm, subFormProviderStrategy);
182             localisedComponents.put(key, component);
183         });
184         return localisedComponents;
185     }
186 
187     private Component doCreateLayout(LayoutDefinition<?> definition, Map<String, Component> components) {
188         final LayoutProducer<LayoutDefinition> producer = getComponentProvider().newInstance(definition.getImplementationClass());
189         final Component layout = producer.createLayout(definition, components);
190         layout.setSizeFull();
191         return layout;
192     }
193 
194     private String resolvePropertyNameByLocale(String name, Locale locale, boolean isI18nProperty) {
195         if (isI18nProperty && !i18NAuthoringSupport.isDefaultLocale(locale)) {
196             return i18NAuthoringSupport.deriveLocalisedPropertyName(name, locale);
197         }
198         return name;
199     }
200 }