View Javadoc
1   /**
2    * This file Copyright (c) 2013-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.form.field.transformer.multi;
35  
36  import info.magnolia.jcr.util.NodeTypes;
37  import info.magnolia.jcr.util.NodeUtil;
38  import info.magnolia.jcr.util.PropertyUtil;
39  import info.magnolia.objectfactory.Components;
40  import info.magnolia.ui.api.i18n.I18NAuthoringSupport;
41  import info.magnolia.ui.form.field.definition.ConfiguredFieldDefinition;
42  import info.magnolia.ui.form.field.transformer.basic.BasicTransformer;
43  import info.magnolia.ui.vaadin.integration.NullItem;
44  import info.magnolia.ui.vaadin.integration.jcr.JcrNewNodeAdapter;
45  import info.magnolia.ui.vaadin.integration.jcr.JcrNodeAdapter;
46  
47  import java.text.DecimalFormat;
48  import java.util.ArrayList;
49  import java.util.HashSet;
50  import java.util.Iterator;
51  import java.util.List;
52  import java.util.Set;
53  
54  import javax.inject.Inject;
55  import javax.jcr.Node;
56  import javax.jcr.RepositoryException;
57  
58  import org.apache.jackrabbit.commons.predicate.Predicate;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  import com.vaadin.v7.data.Item;
63  import com.vaadin.v7.data.Property;
64  import com.vaadin.v7.data.util.ObjectProperty;
65  import com.vaadin.v7.data.util.PropertysetItem;
66  
67  /**
68   * Sub Nodes implementation of {@link info.magnolia.ui.form.field.transformer.Transformer} storing and retrieving properties (as {@link PropertysetItem}) displayed in MultiField.<br>
69   * Storage strategy: <br>
70   * - root node (relatedFormItem)<br>
71   * -- child node 1 (used to store the first value of the MultiField as a property)<br>
72   * --- property1 (store the first value of the MultiField)<br>
73   * -- child node 2 (used to store the second value of the MultiField as a property)<br>
74   * --- property2 (store the second value of the MultiField)<br>
75   * ...<br>
76   * Each element of the MultiField is stored in a property located in a child node of the root node. <br>
77   * Child node name : Incremental number (00, 01,....) <br>
78   * Property name : field name <br>
79   * Override {@link MultiValueChildrenNodeTransformer#createChildItemName(Set, Object, JcrNodeAdapter)} to define the child node name.<br>
80   * Override {@link MultiValueChildrenNodeTransformer#setChildValuePropertyName(String)} to change the property name used to store the MultiField value element.
81   */
82  public class MultiValueChildrenNodeTransformer extends BasicTransformer<PropertysetItem> {
83  
84      private static final Logger log = LoggerFactory.getLogger(MultiValueChildrenNodeTransformer.class);
85  
86      protected String childNodeType = NodeTypes.ContentNode.NAME;
87      private String childValuePropertyName;
88  
89      @Inject
90      public MultiValueChildrenNodeTransformer(Item relatedFormItem, ConfiguredFieldDefinition definition, Class<PropertysetItem> type, I18NAuthoringSupport i18NAuthoringSupport) {
91          super(relatedFormItem, definition, type, i18NAuthoringSupport);
92          this.childValuePropertyName = definition.getName();
93      }
94  
95      /**
96       * @deprecated since 5.4.2 - use {@link #MultiValueChildrenNodeTransformer(Item, ConfiguredFieldDefinition, Class, I18NAuthoringSupport)} instead.
97       */
98      @Deprecated
99      public MultiValueChildrenNodeTransformer(Item relatedFormItem, ConfiguredFieldDefinition definition, Class<PropertysetItem> type) {
100         this(relatedFormItem, definition, type, Components.getComponent(I18NAuthoringSupport.class));
101     }
102 
103     /**
104      * No I18N Support implemented for subNode.
105      */
106     @Override
107     public boolean hasI18NSupport() {
108         return false;
109     }
110 
111     /**
112      * Retrieve a list of values based on the sub nodes.<br>
113      * - get a list of childNodes to handle <br>
114      * - for each childNode retrieve the value to set to the {@link PropertysetItem} <br>
115      * If no childNodes are present, return an empty {@link PropertysetItem}.
116      */
117     @Override
118     public PropertysetItem readFromItem() {
119         PropertysetItem newValues = new PropertysetItem();
120         JcrNodeAdapter rootItem = getRootItem();
121         // Get a list of childNodes
122         List<Node> childNodes = getStoredChildNodes(rootItem);
123         int position = 0;
124         for (Node child : childNodes) {
125             Object value = getValueFromChildNode(child);
126             if (value != null) {
127                 newValues.addItemProperty(position, new ObjectProperty<>(value));
128                 position += 1;
129             }
130         }
131         return newValues;
132     }
133 
134 
135     /**
136      * Create new Child Items based on the newValues. <br>
137      * - on the rootItem, create or update childItems based on the newValues (one ChildItem per new Value).
138      * - remove the no more existing child from the source Item.
139      */
140     @Override
141     public void writeToItem(PropertysetItem newValue) {
142         // Get root Item
143         JcrNodeAdapter rootItem = getRootItem();
144         if (rootItem == null) {
145             // nothing to write yet, someone just clicked add and then clicked away to other field
146             return;
147         }
148         rootItem.getChildren().clear();
149         // Add childItems to the rootItem
150         setNewChildItem(rootItem, newValue);
151         // Remove all no more existing children
152         detachNoMoreExistingChildren(rootItem);
153         // Attach or Detach rootItem from parent
154         handleRootitemAndParent(rootItem);
155     }
156 
157     /**
158      * Define the root Item used in order to set the SubNodes list.
159      */
160     protected JcrNodeAdapter getRootItem() {
161         return (JcrNodeAdapter) relatedFormItem;
162     }
163 
164     /**
165      * Get all childNodes of parent passing the {@link Predicate} created by {@link MultiValueChildrenNodeTransformer#createPredicateToEvaluateChildNode()} or <br>
166      * with type {@link NodeTypes.ContentNode#NAME} if the {@link Predicate} is null.
167      */
168     protected List<Node> getStoredChildNodes(JcrNodeAdapter parent) {
169         List<Node> res = new ArrayList<>();
170         try {
171             if (parent != null && !(parent instanceof JcrNewNodeAdapter) && parent.getJcrItem().hasNodes()) {
172                 Predicate predicate = createPredicateToEvaluateChildNode();
173                 if (predicate != null) {
174                     res = NodeUtil.asList(NodeUtil.getNodes(parent.getJcrItem(), predicate));
175                 } else {
176                     res = NodeUtil.asList(NodeUtil.getNodes(parent.getJcrItem(), childNodeType));
177                 }
178             }
179         } catch (RepositoryException re) {
180             log.warn("Not able to access the Child Nodes of the following Node Identifier {}", parent.getItemId(), re);
181         }
182         return res;
183     }
184 
185     /**
186      * Create a {@link Predicate} used to evaluate the child node of the root to handle.<br>
187      * Only return child node that have a number name's.
188      */
189     protected Predicate createPredicateToEvaluateChildNode() {
190 
191         return node -> {
192             if (node instanceof Node) {
193                 try {
194                     return ((Node) node).getName().matches("[0-9]+");
195                 } catch (RepositoryException e) {
196                     return false;
197                 }
198             }
199             return false;
200         };
201     }
202 
203     /**
204      * Return a specific value from the child node.
205      */
206     protected Object getValueFromChildNode(Node child) {
207         try {
208             if (child.hasProperty(childValuePropertyName)) {
209                 return PropertyUtil.getPropertyValueObject(child, childValuePropertyName);
210             }
211         } catch (RepositoryException re) {
212             log.warn("Not able to access the Child Nodes property of the following Child Node Name {}", NodeUtil.getName(child), re);
213         }
214         return null;
215     }
216 
217 
218     protected void setNewChildItem(JcrNodeAdapter rootItem, PropertysetItem newValue) {
219         // Used to build the ChildItemName;
220         Set<String> childNames = new HashSet<>();
221         Node rootNode = rootItem.getJcrItem();
222         try {
223             Iterator<?> it = newValue.getItemPropertyIds().iterator();
224             while (it.hasNext()) {
225                 Property<?> p = newValue.getItemProperty(it.next());
226                 // Do not handle null values
227                 if (p == null || p.getValue() == null) {
228                     continue;
229                 }
230                 Object value = p.getValue();
231                 // Create the child Item Name
232                 String childName = createChildItemName(childNames, value, rootItem);
233                 // Get or create the childItem
234                 JcrNodeAdapter childItem = initializeChildItem(rootItem, rootNode, childName);
235                 // Set the Value to the ChildItem
236                 setChildItemValue(childItem, value);
237             }
238         } catch (Exception e) {
239             log.warn("Not able to create a Child Item for {} ", rootItem.getItemId(), e);
240         }
241     }
242 
243     /**
244      * Set the value as property to the childItem.
245      */
246     protected void setChildItemValue(JcrNodeAdapter childItem, Object value) {
247         childItem.addItemProperty(childValuePropertyName, new ObjectProperty<>(value));
248     }
249 
250     /**
251      * Create a Child Item.<br>
252      * - if the related node already has a Child Node called 'childName', initialize the ChildItem based on this child Node.<br>
253      * - else create a new JcrNodeAdapter.
254      */
255     protected JcrNodeAdapter initializeChildItem(JcrNodeAdapter rootItem, Node rootNode, String childName) throws RepositoryException {
256         JcrNodeAdapter childItem;
257         if (!(rootItem instanceof JcrNewNodeAdapter) && rootNode.hasNode(childName)) {
258             childItem = new JcrNodeAdapter(rootNode.getNode(childName));
259         } else {
260             childItem = new JcrNewNodeAdapter(rootNode, childNodeType, childName);
261         }
262         rootItem.addChild(childItem);
263         return childItem;
264     }
265 
266     /**
267      * If values are already stored, remove the no more existing one.
268      */
269     private void detachNoMoreExistingChildren(JcrNodeAdapter rootItem) {
270         try {
271             List<Node> children = getStoredChildNodes(rootItem);
272             for (Node child : children) {
273                 if (rootItem.getChild(child.getName()) == null) {
274                     JcrNodeAdapter toRemove = new JcrNodeAdapter(child);
275                     rootItem.removeChild(toRemove);
276                 } else {
277                     detachNoMoreExistingChildren((JcrNodeAdapter) rootItem.getChild(child.getName()));
278                 }
279             }
280         } catch (RepositoryException e) {
281             log.error("Could remove children", e);
282         }
283     }
284 
285     /**
286      * Handle the relation between parent and rootItem.<br>
287      * Typically, if rootItem would be a child of parentItem: <br>
288      * <p>
289      * if (childItem.getChildren() != null && !childItem.getChildren().isEmpty()) { ((JcrNodeAdapter) parent).addChild(childItem); } else { ((JcrNodeAdapter) parent).removeChild(childItem); }
290      * </p>
291      */
292     protected void handleRootitemAndParent(JcrNodeAdapter rootItem) {
293         // In our case, do nothing as childItem is already the parent.
294     }
295 
296     /**
297      * Basic Implementation that create child Nodes with increasing number as Name.
298      */
299     protected String createChildItemName(Set<String> childNames, Object value, JcrNodeAdapter rootItem) {
300         int nb = 0;
301         String name = "00";
302         DecimalFormat df = new DecimalFormat("00");
303         while (childNames.contains(name)) {
304             nb += 1;
305             name = df.format(nb);
306         }
307         childNames.add(name);
308         return name;
309     }
310 
311     public void setChildValuePropertyName(String newName) {
312         this.childValuePropertyName = newName;
313     }
314 
315     /**
316      * Retrieve or create a child node as {@link JcrNodeAdapter}. Method will return null for any none JcrNodeAdapter related form items.
317      */
318     protected JcrNodeAdapter getOrCreateChildNode(String childNodeName, String childNodeType) throws RepositoryException {
319         JcrNodeAdapter child;
320         if (relatedFormItem instanceof NullItem) {
321             return null;
322         }
323         if (!(relatedFormItem instanceof JcrNodeAdapter)) {
324             log.warn("Detected attempt to retrieve a Jcr Item from a Non Jcr Item Adapter. Will return null.");
325             return null;
326         }
327         Node node = ((JcrNodeAdapter) relatedFormItem).getJcrItem();
328         if (node.hasNode(childNodeName) && !(relatedFormItem instanceof JcrNewNodeAdapter)) {
329             child = new JcrNodeAdapter(node.getNode(childNodeName));
330             ((JcrNodeAdapter) relatedFormItem).addChild(child);
331         } else {
332             child = new JcrNewNodeAdapter(node, childNodeType, childNodeName);
333             ((JcrNodeAdapter) relatedFormItem).addChild(child);
334         }
335         return child;
336     }
337 }