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