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.NoopNameDecorator;
39  import info.magnolia.objectfactory.Components;
40  import info.magnolia.ui.api.i18n.I18NAuthoringSupport;
41  import info.magnolia.ui.contentapp.configuration.column.ColumnDefinition;
42  import info.magnolia.ui.field.FieldDefinition;
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.Builder;
63  import lombok.Data;
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  public class JcrItemPropertySet implements PropertySet<Item> {
75  
76      /**
77       * Initialise JCR property set based on a simple map of
78       * property names mapped to their types.
79       *
80       * @param properties property names mapped to their respective types
81       */
82      public static JcrItemPropertySet withProperties(Map<String, Class> properties) {
83          List<JcrItemPropertySet.JcrPropertyDescriptor> propertyDescriptors = properties.entrySet().stream()
84                  .map(entry -> JcrItemPropertySet.JcrPropertyDescriptor.builder()
85                          .type(entry.getValue())
86                          .name(entry.getKey())
87                          .build())
88                  .collect(toList());
89  
90          return new JcrItemPropertySet(propertyDescriptors, Components.getComponent(I18NAuthoringSupport.class));
91      }
92  
93      /**
94       * Initialise JCR property set based on list of column definitions.
95       * Useful for grid row editors.
96       *
97       * @param columnDefinitions list of column definitions used as property set source
98       */
99      public static <T> PropertySet<Item> fromColumns(List<ColumnDefinition<T>> columnDefinitions) {
100         return withProperties(columnDefinitions.stream().collect(toMap(ColumnDefinition::getName, ColumnDefinition::getType)));
101     }
102 
103     /**
104      * Initialise JCR property set based on list of field definitions definitions.
105      * Useful for grid row editors.
106      *
107      * @param fieldDefinitions list of column definitions used as property set source
108      */
109     public static PropertySet<Item> fromFieldDefinitions(Collection<FieldDefinition> fieldDefinitions, Locale locale) {
110         //noinspection unchecked
111         return new JcrItemPropertySet(fieldDefinitions.stream()
112                 .map(fieldDefinition -> JcrPropertyDescriptor
113                         .builder()
114                         .name(fieldDefinition.getName())
115                         .isReadonly(fieldDefinition.isReadOnly())
116                         .type(fieldDefinition.getType())
117                         .build())
118                 .collect(toList()), locale, Components.getComponent(I18NAuthoringSupport.class), null);
119     }
120 
121     private final List<PropertyDefinition<Item, ?>> knownProperties = new ArrayList<>();
122     private final Locale locale;
123     private final I18NAuthoringSupport<Item> genericLocalisationSupport;
124     private final PropertyNameDecorator propertyNameDecorator;
125 
126     public JcrItemPropertySet(List<JcrPropertyDescriptor> knownProperties, I18NAuthoringSupport<Item> genericLocalisationSupport) {
127         this(knownProperties, null, genericLocalisationSupport, new NoopNameDecorator());
128     }
129 
130     public JcrItemPropertySet(List<JcrPropertyDescriptor> knownProperties, Locale locale, I18NAuthoringSupport<Item> genericLocalisationSupport) {
131         this(knownProperties, locale, genericLocalisationSupport, new NoopNameDecorator());
132     }
133 
134     public JcrItemPropertySet(List<JcrPropertyDescriptor> knownProperties, Locale locale, I18NAuthoringSupport<Item> genericLocalisationSupport, PropertyNameDecorator propertyNameDecorator) {
135         this.locale = locale;
136         this.genericLocalisationSupport = genericLocalisationSupport;
137         this.propertyNameDecorator = propertyNameDecorator;
138         //noinspection unchecked
139         knownProperties.forEach(property -> this.knownProperties.add(new JcrItemPropertyDefinition<>(this, property)));
140     }
141 
142     @Override
143     public Stream<PropertyDefinition<Item, ?>> getProperties() {
144         return knownProperties.stream();
145     }
146 
147     @Override
148     public Optional<PropertyDefinition<Item, ?>> getProperty(String name) {
149         return Optional.of(knownProperties.stream()
150                 .filter(def -> def.getName().equals(name))
151                 .findFirst()
152                 .orElse(getDecoratedProperty(name)));
153     }
154 
155     private PropertyDefinition<Item, ?> getDecoratedProperty(String name) {
156         return Optional.of(knownProperties.stream()
157                 .filter(def -> def.getName().equals(propertyNameDecorator.apply(name)))
158                 .findFirst()
159                 .orElse(new JcrItemPropertyDefinition<>(this, JcrItemPropertySet.JcrPropertyDescriptor.builder()
160                         .type(Object.class)
161                         .name(name)
162                         .build()))).get();
163     }
164 
165     /**
166      * Jcr property descriptor.
167      *
168      * @param <T> property type
169      */
170     @Data
171     public static class JcrPropertyDescriptor<T> {
172         private Class<T> type;
173         private String name;
174         private boolean isReadonly;
175         private boolean isI18n;
176         private Locale locale;
177         private T defaultValue;
178         private PropertyNameDecorator propertyNameDecorator = new NoopNameDecorator();
179 
180         public JcrPropertyDescriptor() {
181         }
182 
183         @Builder
184         JcrPropertyDescriptor(Class<T> type, String name, boolean isReadonly, boolean isI18n, Locale locale, T defaultValue, PropertyNameDecorator propertyNameDecorator) {
185             this.type = type;
186             this.name = name;
187             this.isReadonly = isReadonly;
188             this.isI18n = isI18n;
189             this.locale = locale;
190             this.defaultValue = defaultValue;
191             if (propertyNameDecorator != null) {
192                 this.propertyNameDecorator = propertyNameDecorator;
193             }
194         }
195 
196         public String getName() {
197             return this.propertyNameDecorator.apply(this.name);
198         }
199     }
200 
201 
202     /**
203      * Wrapper utility for JCR property descriptors.
204      *
205      * @param <T>
206      *     item type
207      */
208     class JcrPropertyDescriptorWrapper<T> extends JcrPropertyDescriptor<T> {
209 
210         private JcrPropertyDescriptor<T> delegate;
211 
212         public JcrPropertyDescriptorWrapper(JcrPropertyDescriptor<T> delegate) {
213             this.delegate = delegate;
214         }
215 
216         @Override
217         public Class<T> getType() {
218             return delegate.getType();
219         }
220 
221         @Override
222         public void setType(Class<T> type) {
223             delegate.setType(type);
224         }
225 
226         @Override
227         public String getName() {
228             return delegate.getName();
229         }
230 
231         @Override
232         public void setName(String name) {
233             delegate.setName(name);
234         }
235 
236         @Override
237         public boolean isReadonly() {
238             return delegate.isReadonly();
239         }
240 
241         @Override
242         public void setReadonly(boolean isReadonly) {
243             delegate.setReadonly(isReadonly);
244         }
245 
246         @Override
247         public boolean isI18n() {
248             return delegate.isI18n();
249         }
250 
251         @Override
252         public void setI18n(boolean isI18n) {
253             delegate.setI18n(isI18n);
254         }
255 
256         @Override
257         public Locale getLocale() {
258             return delegate.getLocale();
259         }
260 
261         @Override
262         public void setLocale(Locale locale) {
263             delegate.setLocale(locale);
264         }
265 
266         @Override
267         public T getDefaultValue() {
268             return delegate.getDefaultValue();
269         }
270 
271         @Override
272         public void setDefaultValue(T defaultValue) {
273             delegate.setDefaultValue(defaultValue);
274         }
275 
276         @Override
277         public PropertyNameDecorator getPropertyNameDecorator() {
278             return delegate.getPropertyNameDecorator();
279         }
280 
281         @Override
282         public void setPropertyNameDecorator(PropertyNameDecorator propertyNameDecorator) {
283             delegate.setPropertyNameDecorator(propertyNameDecorator);
284         }
285 }
286 
287     /**
288      * Wraps a delegate property descriptor and may append a locale
289      * suffix depending on the node property interacts with and the
290      * locale settings of the related property set.
291      *
292      * @param <T>
293      *     item type
294      */
295     class LocalisedJcrPropertyDescriptor<T> extends JcrPropertyDescriptorWrapper<T> {
296 
297         private final Node relatedNode;
298 
299         LocalisedJcrPropertyDescriptor(JcrPropertyDescriptor<T> delegate, Node relatedNode) {
300             super(delegate);
301             this.relatedNode = relatedNode;
302         }
303 
304         @Override
305         public String getName() {
306             return getName(relatedNode);
307         }
308 
309         public String getName(Node node) {
310             if (locale != null &&
311                     isI18n() &&
312                     !genericLocalisationSupport.isDefaultLocale(locale, node)) {
313                 return genericLocalisationSupport.deriveLocalisedPropertyName(super.getName(), locale);
314             }
315             return super.getName();
316         }
317     }
318 
319     /**
320      * Connects JCR node to a PropertySet's property definitions.
321      * Getters and setters are delegated to JCR via {@link JcrItemInteractionStrategy}.
322      *
323      * @param <V>
324      *     property value type
325      */
326     private class JcrItemPropertyDefinition<V> implements PropertyDefinition<Item, V> {
327 
328         private final PropertySet<Item> propertySet;
329         private final JcrItemPropertySet.JcrPropertyDescriptor<V> descriptor;
330 
331         private JcrItemPropertyDefinition(PropertySet<Item> propertySet, JcrPropertyDescriptor<V> descriptor) {
332             this.propertySet = propertySet;
333             this.descriptor = descriptor;
334         }
335 
336         @Override
337         public ValueProvider<Item, V> getGetter() {
338             return item -> {
339                 JcrPropertyDescriptor<V> descriptor = this.descriptor;
340                 if (item.isNode()) {
341                     descriptor = new LocalisedJcrPropertyDescriptor<>(descriptor, (Node) item);
342                 }
343                 return JcrItemInteractionStrategy.get(item).get(item, descriptor);
344             };
345         }
346 
347         @Override
348         public Optional<Setter<Item, V>> getSetter() {
349             return Optional.of((Setter<Item, V>) (Item item, V value) -> {
350                 JcrPropertyDescriptor<V> descriptor = this.descriptor;
351                 if (item.isNode()) {
352                     descriptor = new LocalisedJcrPropertyDescriptor<>(descriptor, (Node) item);
353                 }
354 
355                 boolean isPropertyUpdatePermitted = !descriptor.isReadonly;
356 
357                 if (!isPropertyUpdatePermitted && item.isNode()) {
358                     Node asNode = (Node) item;
359                     String propertyName = descriptor.getName();
360                     // the field is marked as read-only, but the target node doesn't have the property at all
361                     isPropertyUpdatePermitted = !Exceptions.wrap().get(() -> asNode.hasProperty(propertyName));
362                 }
363 
364                 if (isPropertyUpdatePermitted) {
365                     JcrItemInteractionStrategy.get(item).set(item, value, descriptor);
366                 }
367             });
368         }
369 
370         @Override
371         public Class<V> getType() {
372             return descriptor.getType();
373         }
374 
375         @Override
376         public Class<?> getPropertyHolderType() {
377             return Node.class;
378         }
379 
380         @Override
381         public String getName() {
382             return descriptor.getName();
383         }
384 
385         @Override
386         public String getCaption() {
387             return descriptor.getName();
388         }
389 
390         @Override
391         public PropertySet<Item> getPropertySet() {
392             return propertySet;
393         }
394     }
395 }