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.databinding;
35  
36  import static java.util.stream.Collectors.*;
37  
38  import info.magnolia.jcr.RuntimeRepositoryException;
39  import info.magnolia.jcr.util.NodeNameHelper;
40  import info.magnolia.jcr.util.NodeUtil;
41  import info.magnolia.jcr.util.PropertyUtil;
42  import info.magnolia.objectfactory.Components;
43  import info.magnolia.ui.databinding.JcrItemPropertySet.JcrPropertyDescriptor;
44  import info.magnolia.util.CollectionConversionCapableBeanUtils;
45  import info.magnolia.util.JcrValueConverter;
46  
47  import java.util.ArrayList;
48  import java.util.Arrays;
49  import java.util.Collection;
50  import java.util.List;
51  import java.util.Objects;
52  import java.util.Optional;
53  import java.util.Set;
54  import java.util.stream.Stream;
55  
56  import javax.jcr.InvalidItemStateException;
57  import javax.jcr.Item;
58  import javax.jcr.ItemNotFoundException;
59  import javax.jcr.Node;
60  import javax.jcr.Property;
61  import javax.jcr.RepositoryException;
62  import javax.jcr.Value;
63  
64  import org.apache.commons.beanutils.BeanUtilsBean;
65  import org.apache.commons.beanutils.ConversionException;
66  import org.apache.commons.lang3.StringUtils;
67  import org.slf4j.Logger;
68  import org.slf4j.LoggerFactory;
69  
70  import lombok.SneakyThrows;
71  
72  /**
73   * Implementation of {@link JcrItemInteractionStrategy}. Base implementation is provided as
74   * an abstract class, the concrete impls are in {@link WithNodes} and {@link WithProperties}.
75   *
76   * @param <I>
77   *     actual type of JCR item
78   */
79  abstract class JcrItemInteractionStrategyImpl<I extends Item> implements JcrItemInteractionStrategy<I> {
80  
81      private static final Logger log = LoggerFactory.getLogger(JcrItemInteractionStrategy.class);
82  
83      private BeanUtilsBean beanUtils = new CollectionConversionCapableBeanUtils();
84  
85      @SneakyThrows(RepositoryException.class)
86      <V> V getPropertyValue(Property property, Class<V> type) {
87          try {
88              boolean multiValue = property.isMultiple();
89  
90              if (multiValue) {
91                  // First un-pack all the values to the objects and collect to a set
92                  final Value[] values = property.getValues();
93                  final Set<Object> unpackedValues =
94                          Stream.of(values)
95                                  .map(JcrValueConverter::read)
96                                  .collect(toSet());
97  
98                  // try to convert set to an actual collection
99                  //noinspection unchecked
100                 return (V) beanUtils.getConvertUtils().convert(unpackedValues, type);
101             } else {
102                 return convertToPresentation(JcrValueConverter.read(property.getValue()), type).orElse(null);
103             }
104         } catch (InvalidItemStateException | ItemNotFoundException e) {
105             log.debug("Failed to get the value of property [{}], returning null", property, e);
106             return null;
107         }
108     }
109 
110     <V> void setPropertyValue(Node node, String name, V value) {
111         try {
112             if ("jcrName".equals(name)) {
113                 handleJcrItemNameChange(node, String.valueOf(value));
114             }
115             if (node.hasProperty(name)) {
116                 if (propertyValueEquals(node.getProperty(name), value)) {
117                     return;
118                 }
119             }
120 
121             PropertyUtil.setProperty(node, name, value);
122         } catch (RepositoryException e) {
123             throw new RuntimeRepositoryException(e);
124         }
125     }
126 
127     private <V> boolean propertyValueEquals(Property property, V value) throws RepositoryException {
128         if (!property.isMultiple()) {
129             return Objects.equals(JcrValueConverter.read(property.getValue()), value);
130         } else {
131             final Value[] values = property.getValues();
132             final List<Object> jcrValues = Arrays.stream(values).map(JcrValueConverter::read).collect(toList());
133             return value instanceof Collection && Objects.equals(jcrValues, new ArrayList<>((Collection<?>) value));
134         }
135     }
136 
137     protected <V> Optional<V> convertToPresentation(Object value, Class<V> targetType) {
138         try {
139             //noinspection unchecked
140             return Optional.ofNullable((V) beanUtils.getConvertUtils().convert(value, targetType));
141         } catch (ConversionException e) {
142             return Optional.empty();
143         }
144     }
145 
146     /**
147      * {@link JcrItemInteractionStrategy} implementation for the nodes.
148      */
149     final static class WithNodes extends JcrItemInteractionStrategyImpl<Node> {
150 
151         @Override
152         public <V> V get(Node node, JcrPropertyDescriptor<V> descriptor) {
153             final Property property = PropertyUtil.getPropertyOrNull(node, descriptor.getName());
154             if ("jcrName".equals(descriptor.getName()) && property == null) {
155                 //noinspection unchecked
156                 return (V) NodeUtil.getName(node);
157             }
158             return Optional.ofNullable(property)
159                     .map(prop -> getPropertyValue(prop, descriptor.getType()))
160                     .orElse(null);
161         }
162 
163         @Override
164         public <V> void set(Node node, V value, JcrPropertyDescriptor<V> descriptor) {
165             if (notNullOrEmptyString(value)) {
166                 setPropertyValue(node, descriptor.getName(), value);
167             } else {
168                 // property exists but value is null or empty, remove it.
169                 Optional.ofNullable(PropertyUtil.getPropertyOrNull(node, descriptor.getName())).ifPresent(property -> {
170                     try {
171                         property.remove();
172                     } catch (RepositoryException e) {
173                         throw new RuntimeRepositoryException(e);
174                     }
175                 });
176             }
177         }
178 
179         private boolean notNullOrEmptyString(Object value) {
180             if (value instanceof String) {
181                 return StringUtils.isNotEmpty((String) value);
182             }
183             return value != null;
184         }
185     }
186 
187     /**
188      * {@link JcrItemInteractionStrategy} implementation for properties.
189      */
190     final static class WithProperties extends JcrItemInteractionStrategyImpl<Property> {
191 
192         @Override
193         @SneakyThrows(RepositoryException.class)
194         public <V> V get(Property property, JcrPropertyDescriptor<V> descriptor) {
195             switch (descriptor.getName()) {
196             case "value":
197                 return getPropertyValue(property, descriptor.getType());
198             // properties can only have the name and the value, attempts to bind
199             // e.g. a grid cell to anything else is ignored. One exclusion is the 'jcrName'
200             // property which for the case of JCR property Grid rows will automatically fall back
201             // to Property#getName
202             case "jcrName":
203             case "name":
204                 //noinspection unchecked
205                 return (V) property.getName();
206             default:
207                 return null;
208             }
209         }
210 
211         @Override
212         public <V> void set(Property item, V value, JcrPropertyDescriptor<V> descriptor) {
213             try {
214                 switch (descriptor.getName()) {
215                 case "value":
216                     setPropertyValue(item.getParent(), item.getName(), value);
217                     break;
218                 case "jcrName":
219                 case "name":
220                     handleJcrItemNameChange(item, String.valueOf(value));
221                 }
222             } catch (RepositoryException e) {
223                 log.warn("Failed to set the value [{}] to [{}]...", e);
224             }
225         }
226     }
227 
228     private static final NodeNameHelper nodeNameHelper = Components.getComponent(NodeNameHelper.class);
229 
230     private static void handleJcrItemNameChange(Item jcrItem, String pendingNewName) throws RepositoryException {
231         if (StringUtils.isNotEmpty(pendingNewName) && !Objects.equals(pendingNewName, jcrItem.getName())) {
232             final String sanitisedNewName = nodeNameHelper.getUniqueName(jcrItem.getParent(), pendingNewName);
233             if (jcrItem.isNode()) {
234                 NodeUtil.renameNode((Node) jcrItem, sanitisedNewName);
235             } else {
236                 PropertyUtil.renameProperty((Property) jcrItem, sanitisedNewName);
237             }
238         }
239     }
240 }