View Javadoc
1   /**
2    * This file Copyright (c) 2012-2016 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.vaadin.integration.jcr;
35  
36  import info.magnolia.cms.core.Path;
37  import info.magnolia.jcr.RuntimeRepositoryException;
38  import info.magnolia.jcr.util.NodeUtil;
39  import info.magnolia.jcr.util.PropertyUtil;
40  
41  import java.util.ArrayList;
42  import java.util.HashMap;
43  import java.util.LinkedHashMap;
44  import java.util.LinkedList;
45  import java.util.List;
46  import java.util.Map;
47  import java.util.Map.Entry;
48  
49  import javax.jcr.Item;
50  import javax.jcr.Node;
51  import javax.jcr.NodeIterator;
52  import javax.jcr.RepositoryException;
53  
54  import org.apache.commons.lang3.StringUtils;
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  import com.vaadin.data.Property;
59  
60  /**
61   * Abstract implementation of an {@link com.vaadin.data.Item} wrapping/representing a {@link javax.jcr.Node}. Implements {Property.ValueChangeListener} in order to inform/change JCR
62   * property when a Vaadin property has changed. Access JCR repository for all read Jcr Property.
63   */
64  public abstract class AbstractJcrNodeAdapter extends AbstractJcrAdapter {
65  
66      private static final Logger log = LoggerFactory.getLogger(AbstractJcrNodeAdapter.class);
67  
68      private String primaryNodeType;
69  
70      private final Map<String, AbstractJcrNodeAdapter> children = new LinkedHashMap<String, AbstractJcrNodeAdapter>();
71  
72      private final Map<String, AbstractJcrNodeAdapter> removedChildren = new HashMap<String, AbstractJcrNodeAdapter>();
73  
74      private AbstractJcrNodeAdapter parent;
75  
76      private String nodeName;
77  
78      private boolean childItemChanges = false;
79  
80      public AbstractJcrNodeAdapter(Node jcrNode) {
81          super(jcrNode);
82      }
83  
84      @Override
85      public boolean isNode() {
86          return true;
87      }
88  
89      @Override
90      protected void initCommonAttributes(Item jcrItem) {
91          super.initCommonAttributes(jcrItem);
92          Node node = (Node) jcrItem;
93          try {
94              if (StringUtils.isBlank(primaryNodeType)) {
95                  primaryNodeType = node.getPrimaryNodeType().getName();
96              }
97          } catch (RepositoryException e) {
98              log.error("Could not determine primaryNodeType name of JCR node", e);
99              primaryNodeType = UNIDENTIFIED;
100         }
101     }
102 
103     protected void setPrimaryNodeTypeName(String primaryNodeTypeName) {
104         this.primaryNodeType = primaryNodeTypeName;
105     }
106 
107     /**
108      * Return the Primary node type Name. This Node type is defined based on the related JCR Node.
109      * In case of new Node, the Type is passed during the construction of the new Item or if not
110      * defined, the Type is equivalent to the Parent Node Type.
111      */
112     public String getPrimaryNodeTypeName() {
113         return primaryNodeType;
114     }
115 
116     protected Map<String, AbstractJcrNodeAdapter> getRemovedChildren() {
117         return removedChildren;
118     }
119 
120     /**
121      * Return the corresponding node directly from the JCR repository. <b> The returned Node does
122      * not contains all changes done on the current Item, but it's a representation of the current
123      * stored Jcr node. </b> To get the Jcr Node including the changes done on the current Item, use
124      * applyChanges().
125      */
126     @Override
127     public Node getJcrItem() {
128         return (Node)super.getJcrItem();
129     }
130 
131     /**
132      * Add a new JCR Property.
133      */
134     @Override
135     public boolean addItemProperty(Object id, Property property) {
136         // REMOVE ME: Never called as overrides by sub class.
137         log.debug("Add new Property Item name " + id + " with value " + property.getValue());
138         try {
139             Node node = getJcrItem();
140             String propertyName = (String) id;
141             if (!node.hasProperty(propertyName)) {
142                 // Create Property.
143                 node.setProperty(propertyName, (String) property.getValue());
144                 getChangedProperties().put((String)id, property);
145                 return true;
146             } else {
147                 log.warn("Property " + id + " already exist.");
148                 return false;
149             }
150         } catch (RepositoryException e) {
151             log.error("Unable to add JCR property", e);
152             return false;
153         }
154     }
155 
156     /**
157      * @return the property if it already exist on the JCR Item a new property if this property
158      *         refer to the JCR Node name null if the property doesn't exist yet.
159      */
160     @Override
161     public Property getItemProperty(Object id) {
162         Object value;
163         Class type = String.class;
164         try {
165             final Node jcrNode = getJcrItem();
166             if (!jcrNode.hasProperty((String) id)) {
167                 if (ModelConstants.JCR_NAME.equals(id)) {
168                     value = jcrNode.getName();
169                 } else {
170                     return null;
171                 }
172             } else {
173                 value = PropertyUtil.getPropertyValueObject(jcrNode, String.valueOf(id));
174                 type = value.getClass();
175             }
176         } catch (RepositoryException e) {
177             throw new RuntimeRepositoryException(e);
178         }
179         DefaultProperty property = new DefaultProperty(type, value);
180         getChangedProperties().put((String) id, property);
181         return property;
182     }
183 
184     /**
185      * Returns the JCR Node represented by this Item with changes applied. Updates both properties and child nodes. Will
186      * create new properties, set new values and remove those requested for removal. Child nodes will also be added,
187      * updated or removed.
188      */
189     @Override
190     public Node applyChanges() throws RepositoryException {
191         // get Node from repository
192         Node node = getJcrItem();
193 
194         // Update Node properties and children
195         updateProperties(node);
196         updateChildren(node);
197 
198         return node;
199     }
200 
201     /**
202      * Updates and removes children based on the {@link #children} and {@link #removedChildren} maps.
203      *
204      * TODO: Has been made public as of MGNLUI-3124 resolution. Needs further API improvement (e.g. no-arg version or possibly some other better way to update the JCR node internals).
205      */
206     public void updateChildren(Node node) throws RepositoryException {
207         if (!children.isEmpty()) {
208             List<String> sortedChildIdentifiers = new LinkedList<>();
209             for (AbstractJcrNodeAdapter child : children.values()) {
210                 // Update child node as well
211                 child.applyChanges();
212                 sortedChildIdentifiers.add(child.getJcrItem().getIdentifier());
213             }
214             if (node.getPrimaryNodeType().hasOrderableChildNodes()) {
215                 sortChildren(node, sortedChildIdentifiers);
216             }
217         }
218 
219         // Remove child node if needed
220         if (!removedChildren.isEmpty()) {
221             for (AbstractJcrNodeAdapter removedChild : removedChildren.values()) {
222                 if (node.hasNode(removedChild.getNodeName())) {
223                     node.getNode(removedChild.getNodeName()).remove();
224                 }
225             }
226         }
227     }
228 
229     /**
230      * Sorts the child nodes of {@code node} according to the passed list of identifiers {@code sortedIdentifiers}.
231      */
232     private void sortChildren(Node node, List<String> sortedIdentifiers) throws RepositoryException {
233 
234         List<String> unsortedIdentifiers = new ArrayList<>();
235 
236         for (NodeIterator it = node.getNodes(); it.hasNext();) {
237             unsortedIdentifiers.add(it.nextNode().getIdentifier());
238         }
239 
240         for (int pos = 0; pos < sortedIdentifiers.size(); pos++) {
241             String current = sortedIdentifiers.get(pos);
242             int nodePos = unsortedIdentifiers.indexOf(current);
243 
244             if (nodePos != -1 && nodePos != pos) {
245                 Node nodeToMove = node.getSession().getNodeByIdentifier(current);
246                 Node target = node.getSession().getNodeByIdentifier(unsortedIdentifiers.get(pos));
247 
248                 NodeUtil.moveNodeBefore(nodeToMove, target);
249                 String movedId = unsortedIdentifiers.remove(nodePos);
250                 unsortedIdentifiers.add(pos, movedId);
251             }
252         }
253     }
254 
255     @Override
256     public void updateProperties(Item item) throws RepositoryException {
257         super.updateProperties(item);
258         if (item instanceof Node) {
259             // Remove Properties
260             Node node = (Node) item;
261             for (Entry<String, Property> entry : getRemovedProperties().entrySet()) {
262                 if (node.hasProperty(entry.getKey())) {
263                     node.getProperty(entry.getKey()).remove();
264                 }
265             }
266             getRemovedProperties().clear();
267         }
268     }
269 
270     /**
271      * Update or remove property. Property with flag saveInfo to false will not be updated. Property
272      * can refer to node property (like name, title) or node.MetaData property like
273      * (MetaData/template). Also handle the specific case of node renaming. If property JCR_NAME is
274      * present, Rename the node.
275      * In case the value has changed to null, it will be removed. When being called from {@link #updateProperties}
276      * we have to make sure it is removed before running in here as {@link #removeItemProperty(java.lang.Object)}
277      * is manipulating the {@link #changedProperties} list directly.
278      */
279     @Override
280     protected void updateProperty(Item item, String propertyId, Property property) {
281         if (!(item instanceof Node)) {
282             return;
283         }
284         Node node = (Node) item;
285         if (ModelConstants.JCR_NAME.equals(propertyId)) {
286             String jcrName = (String) property.getValue();
287             try {
288                 if (jcrName != null && !jcrName.isEmpty() && !jcrName.equals(node.getName())) {
289 
290                     // make sure new path is available
291                     jcrName = Path.getValidatedLabel(jcrName);
292                     jcrName = Path.getUniqueLabel(node.getSession(), node.getParent().getPath(), jcrName);
293 
294                     NodeUtil.renameNode(node, jcrName);
295 
296                     setItemId(JcrItemUtil.getItemId(node));
297                 }
298             } catch (RepositoryException e) {
299                 log.error("Could not rename JCR Node.", e);
300             }
301         } else if (propertyId != null && !propertyId.isEmpty()) {
302             if (property.getValue() != null) {
303                 try {
304                     PropertyUtil.setProperty(node, propertyId, property.getValue());
305                 } catch (RepositoryException e) {
306                     log.error("Could not set JCR Property {}", propertyId, e);
307                 }
308             } else {
309                 removeItemProperty(propertyId);
310                 log.debug("Property '{}' has a null value: Will be removed", propertyId);
311             }
312         }
313     }
314 
315     /**
316      * @param nodeName name of the child node
317      * @return child if part of the children, or null if not defined.
318      */
319     public AbstractJcrNodeAdapter getChild(String nodeName) {
320         return children.get(nodeName);
321     }
322 
323     public Map<String, AbstractJcrNodeAdapter> getChildren() {
324         return children;
325     }
326 
327     /**
328      * Add a child adapter to the current Item. <b>Only Child Nodes part of this Map will
329      * be persisted into Jcr.</b>
330      */
331     public AbstractJcrNodeAdapter addChild(AbstractJcrNodeAdapter child) {
332         childItemChanges = true;
333         if (removedChildren.containsKey(child.getNodeName())) {
334             removedChildren.remove(child.getNodeName());
335         }
336         child.setParent(this);
337         if (children.containsKey(child.getNodeName())) {
338             children.remove(child.getNodeName());
339         }
340         return children.put(child.getNodeName(), child);
341     }
342 
343     /**
344      * Remove a child Node from the child list. <b>When removing an JcrNodeAdapter, this child
345      * will be added to the Remove Child List even if this Item was not part of the current children
346      * list. All Item part from the removed list are removed from the Jcr repository.</b>
347      */
348     public boolean removeChild(AbstractJcrNodeAdapter toRemove) {
349         childItemChanges = true;
350         removedChildren.put(toRemove.getNodeName(), toRemove);
351         return children.remove(toRemove.getNodeName()) != null;
352     }
353 
354     /**
355      * Return the current Parent Item (If Item is a child). Parent is set by calling addChild(...
356      */
357     public AbstractJcrNodeAdapter getParent() {
358         return parent;
359     }
360 
361     public void setParent(AbstractJcrNodeAdapter parent) {
362         this.parent = parent;
363     }
364 
365     /**
366      * Return the current Node Name. For new Item, this is the name set in the new Item constructor
367      * or null if not yet defined.
368      */
369     public String getNodeName() {
370         return this.nodeName;
371     }
372 
373     public void setNodeName(String nodeName) {
374         this.nodeName = nodeName;
375     }
376 
377     /**
378      * @return true if an {@link com.vaadin.data.Item} was added or removed, false otherwise.
379      */
380     public boolean hasChildItemChanges() {
381         return childItemChanges;
382     }
383 }