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