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