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.util.NodeNameHelper;
39  import info.magnolia.jcr.util.NodeTypes;
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  
46  import java.io.File;
47  import java.io.IOException;
48  import java.util.ArrayList;
49  import java.util.Collection;
50  import java.util.List;
51  import java.util.Objects;
52  import java.util.Optional;
53  import java.util.stream.Stream;
54  
55  import javax.jcr.Item;
56  import javax.jcr.Node;
57  import javax.jcr.Property;
58  import javax.jcr.PropertyType;
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 static final String JCR_NAME = "jcrName";
77      private final BeanUtilsBean beanUtils = new CollectionConversionCapableBeanUtils();
78  
79      <V> V getPropertyValue(Property property, Class<V> type) {
80          if (!Exceptions.wrap().get(property::isMultiple)) {
81              return convertToPresentation(PropertyUtil.getValueObject(Exceptions.wrap().get(property::getValue)), type).orElse(null);
82          } else {
83              // try to convert set to an actual collection
84              // noinspection unchecked
85              return (V) beanUtils.getConvertUtils().convert(Stream.of(Exceptions.wrap().get(property::getValues))
86                              .map(PropertyUtil::getValueObject)
87                              .collect(toSet()),
88                      type);
89          }
90      }
91  
92      <V> void setPropertyValue(Node node, String name, V value) throws RepositoryException, IOException {
93          if (value instanceof File) {
94              createBinary(node, (File) value);
95          } else if (node.hasProperty(name) && propertyValueEquals(node.getProperty(name), value)) {
96              //no op
97          } else {
98              PropertyUtil.setProperty(node, name, value);
99          }
100     }
101 
102     private <V> boolean propertyValueEquals(Property property, V value) throws RepositoryException {
103         if (property.isMultiple()) {
104             final List<Object> values = Stream.of(property.getValues())
105                     .map(PropertyUtil::getValueObject)
106                     .collect(toList());
107             return value instanceof Collection && Objects.equals(values, new ArrayList<>((Collection<?>) value));
108         } else {
109             return Objects.equals(PropertyUtil.getValueObject(property.getValue()), value);
110         }
111     }
112 
113     protected <V> Optional<V> convertToPresentation(Object value, Class<V> targetType) {
114         try {
115             //noinspection unchecked
116             return Optional.ofNullable((V) beanUtils.getConvertUtils().convert(value, targetType));
117         } catch (ConversionException e) {
118             return Optional.empty();
119         }
120     }
121 
122     /**
123      * {@link JcrItemInteractionStrategy} implementation for the nodes.
124      */
125     final static class WithNodes extends JcrItemInteractionStrategyImpl<Node> {
126 
127         @Override
128         public <V> V get(Node node, JcrPropertyDescriptor<V> descriptor) {
129             Property property;
130             if (File.class.equals(descriptor.getType())) {
131                 return (V) Exceptions.wrap().get(() -> getBinary(node));
132             } else {
133                 property = PropertyUtil.getPropertyOrNull(node, JCR_NAME.equals(descriptor.getName()) ?
134                         Optional.ofNullable(descriptor.getNodeNameProperty()).orElse(JCR_NAME) : //no need to rename jcrName fields when changing JcrDatasource#nodeNameProperty
135                         descriptor.getName()
136                 );
137             }
138             if (shouldReturnRealNodeName(descriptor, property) && !node.isNew()) {
139                 //noinspection unchecked
140                 return (V) NodeUtil.getName(node);
141             }
142             return Optional.ofNullable(property)
143                     .map(prop -> getPropertyValue(prop, descriptor.getType())).orElse(null);
144         }
145 
146         private boolean shouldReturnRealNodeName(JcrPropertyDescriptor<?> descriptor, Property property) {
147             return isNodeNameProperty(descriptor) && (property == null || descriptor.getNodeNameProperty() == null); //either there is no such property or datasource supports only real node name
148         }
149 
150         @Override
151         public <V> void set(Node node, V value, JcrPropertyDescriptor<V> descriptor) {
152             Optional<Property> property = Optional.ofNullable(PropertyUtil.getPropertyOrNull(node, descriptor.getName()));
153             if (notNullOrEmptyString(value)) {
154                 // update property value only in case it is not marked as
155                 // read-only or if is completely missing in corresponding node
156                 // (i.e. persist a default read-only value to JCR)
157                 if (!descriptor.isReadonly() || !property.isPresent()) {
158                     Exceptions.wrap().run(() -> {
159                         if ((isNodeNameProperty(descriptor))) {
160                             handleJcrItemNameChange(node, descriptor.getNodeNameProperty(), String.valueOf(value));
161                         } else {
162                             setPropertyValue(node, descriptor.getName(), value);
163                         }
164                     });
165                 }
166             } else {
167                 // property exists but value is null or empty, remove it unless it is a system property
168                 property.ifPresent(Exceptions.wrap().consumer(prop -> {
169                     if (!prop.getName().startsWith(NodeTypes.JCR_PREFIX)) {
170                         prop.remove();
171                     }
172                 }));
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     private static boolean isNodeNameProperty(JcrPropertyDescriptor<?> descriptor) {
185         return JCR_NAME.equals(descriptor.getName()) || descriptor.getName().equals(descriptor.getNodeNameProperty());
186     }
187 
188     /**
189      * {@link JcrItemInteractionStrategy} implementation for properties.
190      */
191     final static class WithProperties extends JcrItemInteractionStrategyImpl<Property> {
192 
193         @Override
194         public <V> V get(Property property, JcrPropertyDescriptor<V> descriptor) {
195             if (isNodeNameProperty(descriptor)) {
196                 //noinspection unchecked
197                 return (V) Exceptions.wrap().get(property::getName);
198             }
199             switch (descriptor.getName()) {
200             case "value":
201                 return getPropertyValue(property, descriptor.getType());
202             // properties can only have the name and the value, attempts to bind
203             // e.g. a grid cell to anything else is ignored. One exclusion is the 'jcrName'
204             // property which for the case of JCR property Grid rows will automatically fall back
205             // to Property#getName
206             case "jcrType":
207                 return (V) PropertyType.nameFromValue(Exceptions.wrap().get(property::getType));
208             default:
209                 return null;
210             }
211         }
212 
213         @Override
214         public <V> void set(Property item, V value, JcrPropertyDescriptor<V> descriptor) {
215             Exceptions.wrap().run(() -> {
216                 if (isNodeNameProperty(descriptor)) {
217                     //noinspection unchecked
218                     handleJcrItemNameChange(item, descriptor.getNodeNameProperty(), String.valueOf(value));
219                 } else if ("value".equals(descriptor.getName())) {
220                     item.setValue(PropertyUtil.createValue(value, item.getSession().getValueFactory()));
221                 } else {
222                     throw new IllegalArgumentException("Supports setting only value, jcrName or configured node name property: " + descriptor.getName());
223                 }
224             });
225         }
226     }
227 
228     private static final NodeNameHelper nodeNameHelper = Components.getComponent(NodeNameHelper.class);
229 
230     private static void handleJcrItemNameChange(Item jcrItem, String propertyName, String pendingNewName) throws RepositoryException {
231         if (StringUtils.isNotEmpty(pendingNewName)) {
232             final String validatedName = nodeNameHelper.getValidatedName(pendingNewName);
233             if (jcrItem.isNode()) {
234                 final Node item = (Node) jcrItem;
235                 if (!jcrItem.getName().equals(validatedName)) {
236                     NodeUtil.renameNode(item, validatedName);
237                     if (propertyName != null) {
238                         PropertyUtil.setProperty(item, propertyName, pendingNewName);
239                     }
240                 }
241             } else {
242                 if (!jcrItem.getName().equals(validatedName)) {
243                     PropertyUtil.renameProperty((Property) jcrItem, validatedName);
244                 }
245             }
246         }
247     }
248 }