View Javadoc

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