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.objectfactory.Components;
39  import info.magnolia.ui.api.i18n.I18NAuthoringSupport;
40  import info.magnolia.ui.contentapp.configuration.column.ColumnDefinition;
41  import info.magnolia.ui.field.FieldDefinition;
42  import info.magnolia.ui.field.NoopNameDecorator;
43  import info.magnolia.ui.field.WithPropertyNameDecorator.PropertyNameDecorator;
44  
45  import java.util.ArrayList;
46  import java.util.Collection;
47  import java.util.List;
48  import java.util.Locale;
49  import java.util.Map;
50  import java.util.Optional;
51  import java.util.stream.Stream;
52  
53  import javax.jcr.Item;
54  import javax.jcr.Node;
55  
56  import com.machinezoo.noexception.Exceptions;
57  import com.vaadin.data.PropertyDefinition;
58  import com.vaadin.data.PropertySet;
59  import com.vaadin.data.ValueProvider;
60  import com.vaadin.server.Setter;
61  
62  import lombok.Data;
63  import lombok.extern.slf4j.Slf4j;
64  
65  
66  /**
67   * Describes a set of JCR properties that can be used for configuration based on
68   * {@link JcrPropertyDescriptor}. Supports both JCR nodes and properties.
69   *
70   * In order to not clutter this class logic, the actual interaction with JCR items is
71   * delegated to {@link JcrItemInteractionStrategy} (provides separate implementation
72   * for nodes and properties).
73   */
74  @Slf4j
75  public class JcrItemPropertySet implements PropertySet<Item> {
76  
77      /**
78       * Initialise JCR property set based on a simple map of
79       * property names mapped to their types.
80       *
81       * @param properties property names mapped to their respective types
82       * @deprecated since 6.2.4. Use {@link JcrItemPropertySet#JcrItemPropertySet(java.util.List, info.magnolia.ui.api.i18n.I18NAuthoringSupport)} instead.
83       */
84      @Deprecated
85      public static JcrItemPropertySet withProperties(Map<String, Class> properties) {
86          List<JcrItemPropertySet.JcrPropertyDescriptor> propertyDescriptors = properties.entrySet().stream()
87                  .map(entry -> JcrPropertyDescriptor.builder()
88                          .type(entry.getValue())
89                          .name(entry.getKey())
90                          .build())
91                  .collect(toList());
92  
93          return new JcrItemPropertySet(propertyDescriptors, Components.getComponent(I18NAuthoringSupport.class));
94      }
95  
96      /**
97       * Initialise JCR property set based on list of column definitions.
98       * Useful for grid row editors.
99       *
100      * @param columnDefinitions list of column definitions used as property set source
101      * @deprecated since 6.2.4. Use {@link JcrItemPropertySet#JcrItemPropertySet(java.util.List, info.magnolia.ui.api.i18n.I18NAuthoringSupport)} instead.
102      */
103     @Deprecated
104     public static <T> PropertySet<Item> fromColumns(List<ColumnDefinition<T>> columnDefinitions) {
105         return withProperties(columnDefinitions.stream().collect(toMap(ColumnDefinition::getName, ColumnDefinition::getType)));
106     }
107 
108     /**
109      * Initialise JCR property set based on list of field definitions definitions.
110      * Useful for grid row editors.
111      *
112      * @param fieldDefinitions list of column definitions used as property set source
113      */
114     public static PropertySet<Item> fromFieldDefinitions(Collection<FieldDefinition> fieldDefinitions, Locale locale) {
115         //noinspection unchecked
116         return new JcrItemPropertySet(fieldDefinitions.stream()
117                 .map(fieldDefinition -> JcrPropertyDescriptor
118                         .builder()
119                         .name(fieldDefinition.getName())
120                         .isReadonly(fieldDefinition.isReadOnly())
121                         .type(fieldDefinition.getType())
122                         .build())
123                 .collect(toList()), locale, Components.getComponent(I18NAuthoringSupport.class), null);
124     }
125 
126     private final List<PropertyDefinition<Item, ?>> knownProperties = new ArrayList<>();
127     private final Locale locale;
128     private final I18NAuthoringSupport<Item> genericLocalisationSupport;
129     private final PropertyNameDecorator propertyNameDecorator;
130 
131     public JcrItemPropertySet(List<JcrPropertyDescriptor> knownProperties, I18NAuthoringSupport<Item> genericLocalisationSupport) {
132         this(knownProperties, null, genericLocalisationSupport, new NoopNameDecorator());
133     }
134 
135     public JcrItemPropertySet(List<JcrPropertyDescriptor> knownProperties, Locale locale, I18NAuthoringSupport<Item> genericLocalisationSupport) {
136         this(knownProperties, locale, genericLocalisationSupport, new NoopNameDecorator());
137     }
138 
139     public JcrItemPropertySet(List<JcrPropertyDescriptor> knownProperties, Locale locale, I18NAuthoringSupport<Item> genericLocalisationSupport, PropertyNameDecorator propertyNameDecorator) {
140         this.locale = locale;
141         this.genericLocalisationSupport = genericLocalisationSupport;
142         this.propertyNameDecorator = propertyNameDecorator;
143         //noinspection unchecked
144         knownProperties.forEach(property -> this.knownProperties.add(new JcrItemPropertyDefinition<>(this, property)));
145     }
146 
147     @Override
148     public Stream<PropertyDefinition<Item, ?>> getProperties() {
149         return knownProperties.stream();
150     }
151 
152     @Override
153     public Optional<PropertyDefinition<Item, ?>> getProperty(String name) {
154         return Optional.of(knownProperties.stream()
155                 .filter(def -> def.getName().equals(name))
156                 .findFirst()
157                 .orElse(getDecoratedProperty(name)));
158     }
159 
160     private PropertyDefinition<Item, ?> getDecoratedProperty(String name) {
161         return Optional.of(knownProperties.stream()
162                 .filter(def -> def.getName().equals(propertyNameDecorator.apply(name)))
163                 .findFirst()
164                 .orElse(new JcrItemPropertyDefinition<>(this, JcrItemPropertySet.JcrPropertyDescriptor.builder()
165                         .type(Object.class)
166                         .name(name)
167                         .build()))).get();
168     }
169 
170     /**
171      * Jcr property descriptor.
172      *
173      * @param <T> property type
174      */
175     @Data
176     public static class JcrPropertyDescriptor<T> {
177         private Class<T> type;
178         private String name;
179         private String nodeNameProperty;
180         private boolean isReadonly;
181         private boolean isI18n;
182         private Locale locale;
183         private PropertyNameDecorator propertyNameDecorator = new NoopNameDecorator();
184 
185         public JcrPropertyDescriptor() {
186         }
187 
188         JcrPropertyDescriptor(Class<T> type, String name, boolean isReadonly, boolean isI18n, Locale locale, PropertyNameDecorator propertyNameDecorator, String nodeNameProperty) {
189             this.type = type;
190             this.name = name;
191             this.isReadonly = isReadonly;
192             this.isI18n = isI18n;
193             this.locale = locale;
194             this.nodeNameProperty = nodeNameProperty;
195             if (propertyNameDecorator != null) {
196                 this.propertyNameDecorator = propertyNameDecorator;
197             }
198         }
199 
200         public static <T> JcrPropertyDescriptorBuilder<T> builder() {
201             return new JcrPropertyDescriptorBuilder<T>();
202         }
203 
204         public String getName() {
205             return this.propertyNameDecorator.apply(this.name);
206         }
207 
208         /**
209          * @deprecated since 6.2.4, use {@link FormView#applyDefaults()} to facilitate default values.
210          */
211         @Deprecated
212         public void setDefaultValue(T defaultValue) {
213             log.warn("JcrPropertyDescriptor#setDefaultValue is deprecated since 6.2.4, doesn't keep the default value");
214         }
215 
216         /**
217          * @deprecated since 6.2.4, use {@link FormView#applyDefaults()} to facilitate default values.
218          */
219         @Deprecated
220         public T getDefaultValue() {
221             log.warn("JcrPropertyDescriptor#getDefaultValue is deprecated since 6.2.4, doesn't keep the default value configuration and returns null");
222             return null;
223         }
224 
225         /**
226          * Builder type for {@link JcrPropertyDescriptor}.
227          *
228          * @param <T>
229          *      property type
230          */
231         public static class JcrPropertyDescriptorBuilder<T> {
232             private Class<T> type;
233             private String name;
234             private boolean isReadonly;
235             private boolean isI18n;
236             private Locale locale;
237             private PropertyNameDecorator propertyNameDecorator;
238             private String nodeNameProperty;
239 
240             JcrPropertyDescriptorBuilder() {
241             }
242 
243             public JcrPropertyDescriptorBuilder<T> type(Class<T> type) {
244                 this.type = type;
245                 return this;
246             }
247 
248             public JcrPropertyDescriptorBuilder<T> name(String name) {
249                 this.name = name;
250                 return this;
251             }
252 
253             public JcrPropertyDescriptorBuilder<T> isReadonly(boolean isReadonly) {
254                 this.isReadonly = isReadonly;
255                 return this;
256             }
257 
258             public JcrPropertyDescriptorBuilder<T> isI18n(boolean isI18n) {
259                 this.isI18n = isI18n;
260                 return this;
261             }
262 
263             public JcrPropertyDescriptorBuilder<T> locale(Locale locale) {
264                 this.locale = locale;
265                 return this;
266             }
267 
268             public JcrPropertyDescriptorBuilder<T> propertyNameDecorator(PropertyNameDecorator propertyNameDecorator) {
269                 this.propertyNameDecorator = propertyNameDecorator;
270                 return this;
271             }
272 
273             public JcrPropertyDescriptorBuilder<T> nodeNameProperty(String nodeNameProperty) {
274                 this.nodeNameProperty = nodeNameProperty;
275                 return this;
276             }
277 
278             /**
279              * @deprecated since 6.2.4, use {@link FormView#applyDefaults()} to facilitate default values.
280              */
281             @Deprecated
282             public JcrPropertyDescriptorBuilder<T> defaultValue(String ignoredDefaultValue) {
283                 return this;
284             }
285 
286             public JcrPropertyDescriptor<T> build() {
287                 return new JcrPropertyDescriptor<T>(type, name, isReadonly, isI18n, locale, propertyNameDecorator, nodeNameProperty);
288             }
289 
290             public String toString() {
291                 return "JcrItemPropertySet.JcrPropertyDescriptor.JcrPropertyDescriptorBuilder(type=" + this.type + ", name=" + this.name + ", isReadonly=" + this.isReadonly + ", isI18n=" + this.isI18n + ", locale=" + this.locale + ", propertyNameDecorator=" + this.propertyNameDecorator + ", nodeNameProperty=" + this.nodeNameProperty + ")";
292             }
293         }
294     }
295 
296 
297     /**
298      * Wrapper utility for JCR property descriptors.
299      *
300      * @param <T>
301      *     item type
302      */
303     class JcrPropertyDescriptorWrapper<T> extends JcrPropertyDescriptor<T> {
304 
305         private JcrPropertyDescriptor<T> delegate;
306 
307         public JcrPropertyDescriptorWrapper(JcrPropertyDescriptor<T> delegate) {
308             this.delegate = delegate;
309         }
310 
311         @Override
312         public Class<T> getType() {
313             return delegate.getType();
314         }
315 
316         @Override
317         public void setType(Class<T> type) {
318             delegate.setType(type);
319         }
320 
321         @Override
322         public String getName() {
323             return delegate.getName();
324         }
325 
326         @Override
327         public void setName(String name) {
328             delegate.setName(name);
329         }
330 
331         @Override
332         public boolean isReadonly() {
333             return delegate.isReadonly();
334         }
335 
336         @Override
337         public void setReadonly(boolean isReadonly) {
338             delegate.setReadonly(isReadonly);
339         }
340 
341         @Override
342         public boolean isI18n() {
343             return delegate.isI18n();
344         }
345 
346         @Override
347         public void setI18n(boolean isI18n) {
348             delegate.setI18n(isI18n);
349         }
350 
351         @Override
352         public Locale getLocale() {
353             return delegate.getLocale();
354         }
355 
356         @Override
357         public void setLocale(Locale locale) {
358             delegate.setLocale(locale);
359         }
360 
361         @Override
362         public PropertyNameDecorator getPropertyNameDecorator() {
363             return delegate.getPropertyNameDecorator();
364         }
365 
366         @Override
367         public void setPropertyNameDecorator(PropertyNameDecorator propertyNameDecorator) {
368             delegate.setPropertyNameDecorator(propertyNameDecorator);
369         }
370 
371         @Override
372         public String getNodeNameProperty() {
373             return delegate.getNodeNameProperty();
374         }
375 
376         @Override
377         public void setNodeNameProperty(String nodeNameProperty) {
378             delegate.setNodeNameProperty(nodeNameProperty);
379         }
380     }
381 
382     /**
383      * Wraps a delegate property descriptor and may append a locale
384      * suffix depending on the node property interacts with and the
385      * locale settings of the related property set.
386      *
387      * @param <T>
388      *     item type
389      */
390     class LocalisedJcrPropertyDescriptor<T> extends JcrPropertyDescriptorWrapper<T> {
391 
392         private final Node relatedNode;
393 
394         LocalisedJcrPropertyDescriptor(JcrPropertyDescriptor<T> delegate, Node relatedNode) {
395             super(delegate);
396             this.relatedNode = relatedNode;
397         }
398 
399         @Override
400         public String getName() {
401             return getName(relatedNode);
402         }
403 
404         public String getName(Node node) {
405             if (locale != null &&
406                     isI18n() &&
407                     !genericLocalisationSupport.isDefaultLocale(locale, node)) {
408                 return genericLocalisationSupport.deriveLocalisedPropertyName(super.getName(), locale);
409             }
410             return super.getName();
411         }
412     }
413 
414     /**
415      * Connects JCR node to a PropertySet's property definitions.
416      * Getters and setters are delegated to JCR via {@link JcrItemInteractionStrategy}.
417      *
418      * @param <V>
419      *     property value type
420      */
421     private class JcrItemPropertyDefinition<V> implements PropertyDefinition<Item, V> {
422 
423         private final PropertySet<Item> propertySet;
424         private final JcrItemPropertySet.JcrPropertyDescriptor<V> descriptor;
425 
426         private JcrItemPropertyDefinition(PropertySet<Item> propertySet, JcrPropertyDescriptor<V> descriptor) {
427             this.propertySet = propertySet;
428             this.descriptor = descriptor;
429         }
430 
431         @Override
432         public ValueProvider<Item, V> getGetter() {
433             return item -> {
434                 JcrPropertyDescriptor<V> descriptor = this.descriptor;
435                 if (item.isNode()) {
436                     descriptor = new LocalisedJcrPropertyDescriptor<>(descriptor, (Node) item);
437                 }
438                 return JcrItemInteractionStrategy.get(item).get(item, descriptor);
439             };
440         }
441 
442         @Override
443         public Optional<Setter<Item, V>> getSetter() {
444             return Optional.of((Setter<Item, V>) (Item item, V value) -> {
445                 JcrPropertyDescriptor<V> descriptor = this.descriptor;
446                 if (item.isNode()) {
447                     descriptor = new LocalisedJcrPropertyDescriptor<>(descriptor, (Node) item);
448                 }
449 
450                 boolean isPropertyUpdatePermitted = !descriptor.isReadonly;
451 
452                 if (!isPropertyUpdatePermitted && item.isNode()) {
453                     Node asNode = (Node) item;
454                     String propertyName = descriptor.getName();
455                     // the field is marked as read-only, but the target node doesn't have the property at all
456                     isPropertyUpdatePermitted = !Exceptions.wrap().get(() -> asNode.hasProperty(propertyName));
457                 }
458 
459                 if (isPropertyUpdatePermitted) {
460                     JcrItemInteractionStrategy.get(item).set(item, value, descriptor);
461                 }
462             });
463         }
464 
465         @Override
466         public Class<V> getType() {
467             return descriptor.getType();
468         }
469 
470         @Override
471         public Class<?> getPropertyHolderType() {
472             return Node.class;
473         }
474 
475         @Override
476         public String getName() {
477             return descriptor.getName();
478         }
479 
480         @Override
481         public String getCaption() {
482             return descriptor.getName();
483         }
484 
485         @Override
486         public PropertySet<Item> getPropertySet() {
487             return propertySet;
488         }
489     }
490 }