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.editor;
35  
36  import static java.util.stream.Collectors.*;
37  
38  import info.magnolia.ui.field.EditorPropertyDefinition;
39  import info.magnolia.ui.field.FieldDefinition;
40  import info.magnolia.ui.field.ValueBoundProperty;
41  import info.magnolia.ui.framework.layout.FieldLayoutDefinition;
42  import info.magnolia.ui.framework.layout.FieldLayoutProducer;
43  import info.magnolia.ui.framework.layout.LayoutDefinition;
44  
45  import java.util.ArrayList;
46  import java.util.HashMap;
47  import java.util.LinkedHashMap;
48  import java.util.List;
49  import java.util.Locale;
50  import java.util.Map;
51  import java.util.Optional;
52  import java.util.function.Function;
53  import java.util.stream.Stream;
54  
55  import javax.inject.Inject;
56  
57  import com.vaadin.data.Binder;
58  import com.vaadin.data.BinderValidationStatus;
59  import com.vaadin.ui.Component;
60  import com.vaadin.ui.ComponentContainer;
61  import com.vaadin.ui.HasComponents;
62  
63  import lombok.AllArgsConstructor;
64  import lombok.Getter;
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>, ItemEditor {
74  
75      private final List<SubEditorReference<T>> subEditors = new ArrayList<>();
76      private final LocaleContext localeContext;
77  
78      private final FormDefinition<T> formDefinition;
79      private final FormPresenter<T> formPresenter;
80      private Component layout;
81  
82      @Inject
83      public FormView(FormDefinition<T> formDefinition, LocaleContext localeContext) {
84  
85          this.formDefinition = formDefinition;
86          this.localeContext = localeContext;
87  
88          this.formPresenter = create(FormPresenter.class, formDefinition, localeContext.getDefault());
89          this.createComplexProperties();
90  
91          if (!localeContext.current().value().isPresent()) {
92              displayLocalisedVersion(localeContext.getDefault());
93          }
94  
95          localeContext.current().observe(optionalLocale -> optionalLocale.ifPresent(this::displayLocalisedVersion));
96      }
97  
98      private void createComplexProperties() {
99          formDefinition.getSubFormDefinitions().forEach(def -> {
100             ComplexPropertyDefinition<T> asComplexPropertyDef = def;
101             ItemProviderStrategy<T, T> subFormProviderStrategy = this.create(asComplexPropertyDef.getItemProvider(), def);
102             Map<Locale, EditorView<T>> localisedRepresentations = new HashMap<>();
103             // in case complex property is localised, keep separate "single-language" representations
104             // each of which can be bound to different child items
105             if (asComplexPropertyDef.isI18n()) {
106                 localeContext.availableLocales().nullableValue().forEach(locale -> {
107                     LocaleContext childLocaleContext = LocaleContext.of(locale);
108                     EditorView<T> subForm = createSubEditor(asComplexPropertyDef, childLocaleContext, subFormProviderStrategy);
109                     localisedRepresentations.put(locale, subForm);
110                 });
111             } else {
112                 // in case complex property is not localised - there is no need to keep localised representations, one is enough,
113                 // just propagate the locale context inside into it.
114                 localisedRepresentations.put(localeContext.getDefault(), createSubEditor(asComplexPropertyDef, this.localeContext, subFormProviderStrategy));
115             }
116 
117             this.subEditors.add(new SubEditorReference<>(localisedRepresentations, subFormProviderStrategy, asComplexPropertyDef));
118         });
119     }
120 
121     private EditorView<T> createSubEditor(ComplexPropertyDefinition<T> definition, LocaleContext localeContext, Object... args) {
122         EditorView<T> subEditor = this.create(definition.getEditorDefinition(), localeContext, args);
123         String caption = definition.getLabel();
124         if (definition.isI18n() && this.localeContext.getAvailableLocales().count() > 1) {
125             subEditor.bindInstance(LocaleContext.class, localeContext);
126             caption = String.format("%s (%s)", caption, localeContext.getDefault().toString());
127         }
128         subEditor.asVaadinComponent().addStyleName(definition.getStyleName());
129         subEditor.asVaadinComponent().setCaption(caption);
130         return subEditor;
131     }
132 
133     @Override
134     public <V> Optional<V> getPropertyValue(String propertyName) {
135         return this.getPropertyValue(propertyName, localeContext.getCurrent());
136     }
137 
138     @Override
139     public void populate(T item) {
140         formPresenter.bind(item);
141         subEditors.forEach(ref -> {
142             ItemProviderStrategy<T, T> itemProviderStrategy = ref.itemProviderStrategy;
143             ref.representations.forEach((locale, subForm) -> itemProviderStrategy.read(item, locale).ifPresent(subForm::populate));
144         });
145     }
146 
147     @Override
148     public void write(T localisedItemSources) {
149         formPresenter.writeBindings(localisedItemSources);
150         subEditors.forEach(ref -> {
151             ItemProviderStrategy<T, T> itemProviderStrategy = ref.itemProviderStrategy;
152             ref.representations.forEach((locale, subForm) -> itemProviderStrategy.read(localisedItemSources, locale).ifPresent(subForm::write));
153         });
154     }
155 
156     @Override
157     public void applyDefaults() {
158         this.formPresenter.applyDefaults();
159         this.subEditors.stream()
160                 .map(ref -> ref.getRepresentations().get(localeContext.getDefault()))
161                 .forEach(EditorView::applyDefaults);
162     }
163 
164     private void displayLocalisedVersion(Locale locale) {
165         formPresenter.getBoundFields(locale).ifPresent(boundFields -> {
166             Component old = this.layout;
167             this.layout = computeLayout(boundFields);
168             HasComponents parent = old != null ? old.getParent() : null;
169             if (parent instanceof ComponentContainer) {
170                 this.layout.addStyleNames(old.getStyleName());
171                 ((ComponentContainer) parent).replaceComponent(old, this.layout);
172             }
173         });
174     }
175 
176     /**
177      * Validates this forms and all its sub-forms recursively.
178      * @return a list of {@link BinderValidationStatus}
179      */
180     @Override
181     public List<BinderValidationStatus<?>> validate() {
182         final Stream<BinderValidationStatus<T>> fieldValidationResults = formPresenter.validateBoundProperties();
183 
184         final Stream<BinderValidationStatus<?>> subFormValidationResults = subEditors.stream()
185                 .flatMap(subEditorReference -> subEditorReference.representations.values().stream())
186                 .flatMap(childForm -> childForm.validate().stream());
187 
188         return Stream.concat(fieldValidationResults, subFormValidationResults).collect(toList());
189     }
190 
191     @Override
192     public <V> Optional<V> getPropertyValue(String propertyName, Locale locale) {
193         return this.formPresenter.getBoundPropertyValue(propertyName, locale);
194     }
195 
196     @Override
197     public Stream<String> getPropertyNames() {
198         return formDefinition.getProperties().stream().map(EditorPropertyDefinition::getName);
199     }
200 
201     private Component computeLayout(Map<FieldDefinition<T>, Component> boundFields) {
202 
203         FieldLayoutDefinition<?> layoutDefinition = formDefinition.getLayout();
204         @SuppressWarnings("unchecked")
205         FieldLayoutProducer<LayoutDefinition> producer = getComponentProvider().newInstance(layoutDefinition.getImplementationClass());
206 
207         Map<String, Component> fieldComponents = new LinkedHashMap<>();
208         subEditors.forEach(subForm -> {
209             Locale targetLocale = subForm.definition.isI18n() ? localeContext.getCurrent() : localeContext.getDefault();
210             fieldComponents.put(subForm.definition.getName(), subForm.representations.get(targetLocale).asVaadinComponent());
211         });
212 
213         boundFields.forEach((def, component) -> fieldComponents.put(def.getName(), component));
214         Map<EditorPropertyDefinition, Component> orderedComponents = this.formDefinition.getProperties().stream().collect(
215                 toMap(Function.identity(),
216                       def -> fieldComponents.get(def.getName()),
217                       (f, s) -> s, LinkedHashMap::new));
218 
219         return producer.createLayout(layoutDefinition, orderedComponents);
220     }
221 
222     @Override
223     public Component asVaadinComponent() {
224         return this.layout;
225     }
226 
227     @Getter
228     @AllArgsConstructor
229     private static class SubEditorReference<T> {
230         private Map<Locale, EditorView<T>> representations;
231         private ItemProviderStrategy<T, T> itemProviderStrategy;
232         private ComplexPropertyDefinition<T> definition;
233     }
234 }