1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.ui.editor;
35
36 import static java.util.function.Function.identity;
37 import static java.util.stream.Collectors.toMap;
38
39 import info.magnolia.objectfactory.ComponentProvider;
40 import info.magnolia.ui.datasource.PropertySetFactory;
41 import info.magnolia.ui.field.FieldBinder;
42 import info.magnolia.ui.field.FieldDefinition;
43 import info.magnolia.ui.framework.ioc.UiComponentProvider;
44 import info.magnolia.ui.vaadin.form.field.FieldLayout;
45
46 import java.lang.reflect.Field;
47 import java.util.Collection;
48 import java.util.HashMap;
49 import java.util.LinkedHashMap;
50 import java.util.Locale;
51 import java.util.Map;
52 import java.util.Objects;
53 import java.util.Optional;
54 import java.util.stream.Stream;
55
56 import javax.inject.Inject;
57
58 import com.vaadin.data.Binder;
59 import com.vaadin.data.BinderValidationStatus;
60 import com.vaadin.data.HasValue;
61 import com.vaadin.data.PropertySet;
62 import com.vaadin.data.ValidationException;
63 import com.vaadin.server.ClientConnector;
64 import com.vaadin.ui.Component;
65
66
67
68
69
70 public class FormPresenter<T> {
71
72 private static final Field changedBindingsAccessor;
73
74 static {
75 try {
76 changedBindingsAccessor = Binder.class.getDeclaredField("changedBindings");
77 changedBindingsAccessor.setAccessible(true);
78 } catch (NoSuchFieldException e) {
79 throw new RuntimeException("Failed to enable changed bindings accessor", e);
80 }
81 }
82
83 private final PropertySetFactory<T> propertySetFactory;
84 private final FormDefinition<T> formDefinition;
85 private final UiComponentProvider componentProvider;
86 private final LocaleContext localeContext;
87 private final Locale defaultLocale;
88 private final Map<Locale, Binder<T>> localisedBinders = new HashMap<>();
89
90 @Inject
91 public FormPresenter(PropertySetFactory<T> propertySetFactory, FormDefinition<T> formDefinition, UiComponentProvider componentProvider, LocaleContext localeContext) {
92 this.propertySetFactory = propertySetFactory;
93 this.formDefinition = formDefinition;
94 this.componentProvider = componentProvider;
95 this.defaultLocale = localeContext.getDefault();
96 this.localeContext = localeContext;
97 this.localeContext.getAvailableLocales().forEach(locale -> this.localisedBinders.put(locale, bindFields(locale)));
98 }
99
100 public FormPresenter(PropertySetFactory<T> propertySetFactory, FormDefinition<T> formDefinition, UiComponentProvider componentProvider) {
101 this(propertySetFactory, formDefinition, componentProvider, LocaleContext.of(Locale.getDefault()));
102 }
103
104 public void bind(T item) {
105 localeContext.getAvailableLocales().map(this::getBinder).forEach(binder -> binder.readBean(item));
106 }
107
108 public Binder<T> getBinder(Locale locale) {
109 return this.localisedBinders.computeIfAbsent(locale, l -> bindFields(locale));
110 }
111
112 public <V> Optional<V> getBoundPropertyValue(String propertyName, Locale locale) {
113 Binder<T> binder = localisedBinders.get(locale);
114
115 Optional<Binder.Binding<T, ?>> binding = binder.getBinding(propertyName);
116 return binding
117 .filter(b -> !b.validate().isError())
118 .map(b -> (V) b.getField().getValue());
119 }
120
121 public void writeBindings(T item) {
122 this.localisedBinders.forEach((locale, binder) -> {
123 try {
124 if (binder.hasChanges()) {
125 binder.writeBean(item);
126 }
127 } catch (ValidationException e) {
128 throw new IllegalStateException("Failed to write form state", e);
129 }
130 });
131 }
132
133 public Stream<BinderValidationStatus<T>> validateBoundProperties() {
134 Stream.Builder<BinderValidationStatus<T>> result = Stream.builder();
135 localisedBinders.forEach((locale, binder) -> {
136 if (requiresValidation(binder, locale)) {
137 result.add(binder.validate());
138 }
139 });
140 return result.build();
141 }
142
143 private boolean requiresValidation(Binder<T> binder, Locale locale) {
144 return Objects.equals(locale, this.defaultLocale) || hasI18NPropertyChanges(binder);
145 }
146
147 public Optional<Map<FieldDefinition<T>, Component>> getBoundFields(Locale locale) {
148 Map<FieldDefinition<T>, Component> result = new LinkedHashMap<>();
149
150 Binder<T> binder = getBinder(locale);
151
152 formDefinition.getFieldDefinitions().forEach(fieldDefinition -> {
153 binder.getBinding(fieldDefinition.getName())
154 .map(Binder.Binding::getField)
155 .ifPresent(wrappedField -> result.put(fieldDefinition, (Component) wrappedField));
156 });
157
158 return Optional.of(result);
159 }
160
161 private Binder<T> createBinder(FormDefinition<T> formDefinition, Locale locale) {
162 return Binder.withPropertySet(createPropertySet(formDefinition, locale));
163 }
164
165 private PropertySet<T> createPropertySet(FormDefinition<T> formDefinition, Locale locale) {
166 return propertySetFactory.fromFormDefinition(formDefinition, locale);
167 }
168
169 private Binder.BindingBuilder configureBinding(FieldDefinition property, Binder.BindingBuilder bindingBuilder) {
170 ComponentProvider fieldComponentProvider = componentProvider.inChildContext(property);
171 FieldBinder<?> fieldBinder = (FieldBinder<?>) fieldComponentProvider.newInstance(property.getFieldBinderClass());
172 return fieldBinder.configureBinding(property, bindingBuilder);
173 }
174
175 private Binder.BindingBuilder bindValidator(Binder.BindingBuilder bindingBuilder) {
176 return bindingBuilder.withValidationStatusHandler(status ->
177 findParent(bindingBuilder.getField(), FieldLayout.class).ifPresent(fieldLayout ->
178 fieldLayout.getValidationStatusHandler()
179 .accept(status.getMessage()
180 .orElse(""))));
181 }
182
183 public void bindProperty(Component field, FieldDefinition property, Binder<T> binder) {
184 configureBinding(property, bindValidator(binder.forField((HasValue) field))).bind(property.getName());
185 }
186
187 private <V extends Component> Optional<V> findParent(HasValue field, Class<V> parentClass) {
188 return findParent((Component) field, parentClass);
189 }
190
191 private <V extends Component> Optional<V> findParent(Component field, Class<V> parentClass) {
192 ClientConnector connector = field;
193 while (connector != null) {
194 if (parentClass.isInstance(connector)) {
195 return Optional.of((V) connector);
196 }
197 connector = connector.getParent();
198 }
199 return Optional.empty();
200 }
201
202 private void broadcastNonLocalisedFieldChanges(Binder<T> binder, Map<Component, FieldDefinition> fields) {
203
204
205
206
207 binder.addValueChangeListener(e -> {
208 Optional<FieldDefinition> fieldDefinition = findRelatedDefinition((Component) e.getSource(), fields);
209 fieldDefinition.ifPresent(def -> {
210 if (!def.isI18n()) {
211 localisedBinders.values().stream()
212 .filter(b -> b != binder)
213 .forEach(b -> b.getBinding(def.getName())
214 .ifPresent(binding -> ((HasValue) binding.getField()).setValue(e.getValue())));
215 }
216 });
217 });
218 }
219
220
221
222
223
224
225 private Optional<FieldDefinition> findRelatedDefinition(Component component, Map<Component, FieldDefinition> parentComponentMappings) {
226 while (component != null) {
227 if (parentComponentMappings.containsKey(component)) {
228 return Optional.of(parentComponentMappings.get(component));
229 }
230 component = component.getParent();
231 }
232 return Optional.empty();
233 }
234
235 protected Binder<T> bindFields(Locale locale) {
236 Map<Component, FieldDefinition> fields = this.formDefinition
237 .getFieldDefinitions()
238 .stream()
239 .map(fieldDefinition -> (FieldDefinition<?>) fieldDefinition)
240 .collect(toMap(
241 fieldDefinition ->
242 (Component) componentProvider
243 .inChildContext(fieldDefinition)
244 .newInstance(fieldDefinition.getFactoryClass())
245 .createField(),
246 identity()));
247
248 Binder<T> binder = createBinder(formDefinition, locale);
249
250 fields.forEach((field, property) -> bindProperty(field, property, binder));
251
252 binder.getFields()
253 .map(field -> (Component) field)
254 .filter(field -> fields.get(field).isI18n())
255 .forEach(component -> {
256 if (locale != null && localeContext.getAvailableLocales().count() > 1) {
257 component.setCaption(String.format("%s (%s)", component.getCaption(), locale.toString()));
258 }
259 });
260
261 broadcastNonLocalisedFieldChanges(binder, fields);
262
263 return binder;
264 }
265
266 private boolean hasI18NPropertyChanges(Binder<T> binder) {
267 Collection<Binder.Binding<T, ?>> changedBindings = getChangedBindings(binder);
268 return formDefinition.getFieldDefinitions().stream()
269 .filter(FieldDefinition::isI18n)
270 .map(FieldDefinition::getName)
271 .map(binder::getBinding)
272 .anyMatch(maybeBinding -> maybeBinding
273 .map(changedBindings::contains)
274 .orElse(false));
275 }
276
277 public Collection<Binder.Binding<T, ?>> getChangedBindings(Binder<T> binder) {
278 try {
279 return ((Collection<Binder.Binding<T, ?>>) changedBindingsAccessor.get(binder));
280 } catch (IllegalAccessException e) {
281 throw new RuntimeException("Failed to access changed bindings", e);
282 }
283 }
284 }