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