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.workbench.tree.drop;
35  
36  import static info.magnolia.jcr.util.NodeUtil.*;
37  
38  import info.magnolia.jcr.util.NodeUtil;
39  import info.magnolia.ui.vaadin.integration.jcr.JcrItemAdapter;
40  import info.magnolia.ui.vaadin.integration.jcr.JcrItemUtil;
41  import info.magnolia.ui.workbench.tree.HierarchicalJcrContainer;
42  import info.magnolia.ui.workbench.tree.MoveHandler;
43  import info.magnolia.ui.workbench.tree.MoveLocation;
44  
45  import java.util.Arrays;
46  import java.util.Collection;
47  import java.util.Iterator;
48  import java.util.Set;
49  
50  import javax.jcr.Item;
51  import javax.jcr.Node;
52  import javax.jcr.Property;
53  import javax.jcr.RepositoryException;
54  
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  import com.vaadin.event.Transferable;
59  import com.vaadin.event.dd.DragAndDropEvent;
60  import com.vaadin.event.dd.DropHandler;
61  import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
62  import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion;
63  import com.vaadin.shared.ui.dd.VerticalDropLocation;
64  import com.vaadin.v7.event.DataBoundTransferable;
65  import com.vaadin.v7.ui.AbstractSelect.AbstractSelectTargetDetails;
66  import com.vaadin.v7.ui.TreeTable;
67  
68  /**
69   * Vaadin {@link DropHandler} for moving JCR nodes and properties in trees, with Drag and Drop.
70   * <p>
71   * Additionally, the dropping conditions can be restricted, by implementing a {@link DropConstraint}
72   * and configuring it in the {@link info.magnolia.ui.workbench.definition.WorkbenchDefinition WorkbenchDefinition}.
73   */
74  public class TreeViewDropHandler implements MoveHandler, DropHandler {
75  
76      private final Logger log = LoggerFactory.getLogger(getClass());
77  
78      private TreeTable tree;
79      private DropConstraint constraint;
80      private AcceptCriterion serverSideCriterion;
81  
82      public TreeViewDropHandler() {
83          createAcceptCriterion();
84      }
85  
86      public TreeViewDropHandler(TreeTable tree, DropConstraint constraint) {
87          this.tree = tree;
88          this.constraint = constraint;
89          createAcceptCriterion();
90      }
91  
92      @Override
93      public void drop(DragAndDropEvent dropEvent) {
94          // Called whenever a drop occurs on the component
95  
96          // Make sure the drag source is the same tree
97          Transferable t = dropEvent.getTransferable();
98  
99          // First acceptance criteria.
100         // Make sure the drag source is the same tree
101         if (t.getSourceComponent() != tree || !(t instanceof DataBoundTransferable)) {
102             return;
103         }
104 
105         AbstractSelectTargetDetails target = (AbstractSelectTargetDetails) dropEvent.getTargetDetails();
106         // Get id's of the target item
107         Object targetItemId = target.getItemIdOver();
108         // On which side of the target the item was dropped
109         VerticalDropLocation location = target.getDropLocation();
110         if (location == null) {
111             log.debug("DropLocation is null. Do nothing.");
112             return;
113         }
114         // Get id's of the dragged items
115         Iterator<Object> selected = getItemIdsToMove(dropEvent).iterator();
116         while (selected.hasNext()) {
117             Object sourceItemId = selected.next();
118             moveNode(sourceItemId, targetItemId, location);
119         }
120     }
121 
122     /**
123      * Returns a collection of itemIds to move:
124      * <ul>
125      * <li>all <em>selected</em> itemIds if and only if the dragging node is <em>also</em> selected</li>
126      * <li>only the dragging itemId if it's not selected</li>.
127      * </ul>
128      */
129     private Collection<Object> getItemIdsToMove(DragAndDropEvent dropEvent) {
130         Transferable t = dropEvent.getTransferable();
131         Object draggingItemId = ((DataBoundTransferable) t).getItemId();
132 
133         // all selected itemIds if and only if the dragging node is also selected
134         Set<Object> selectedItemIds = (Set<Object>) ((TreeTable) t.getSourceComponent()).getValue();
135         if (selectedItemIds.contains(draggingItemId)) {
136             return selectedItemIds;
137         }
138 
139         // only the dragging itemId if it's not selected
140         return Arrays.asList(draggingItemId);
141     }
142 
143     /**
144      * Accept dragged Elements.
145      */
146     @Override
147     public AcceptCriterion getAcceptCriterion() {
148         return serverSideCriterion;
149     }
150 
151     /**
152      * Move a node within a tree onto, above or below another node depending on
153      * the drop location.
154      *
155      * @param sourceItemId
156      * id of the item to move
157      * @param targetItemId
158      * id of the item onto which the source node should be moved
159      * @param location
160      * VerticalDropLocation indicating where the source node was
161      * dropped relative to the target node
162      */
163     private void moveNode(Object sourceItemId, Object targetItemId, VerticalDropLocation location) {
164         log.debug("DropLocation: {}", location.name());
165         // Get Item from tree
166         HierarchicalJcrContainer container = (HierarchicalJcrContainer) tree.getContainerDataSource();
167         JcrItemAdapter sourceItem = (JcrItemAdapter) container.getItem(sourceItemId);
168         JcrItemAdapter targetItem = (JcrItemAdapter) container.getItem(targetItemId);
169 
170         // Sorting goes as
171         // - If dropped ON a node, we append it as a child
172         // - If dropped on the TOP part of a node, we move/add it before
173         // the node
174         // - If dropped on the BOTTOM part of a node, we move/add it
175         // after the node
176 
177         if (location == VerticalDropLocation.MIDDLE) {
178             if (constraint.allowedAsChild(sourceItem, targetItem)) {
179                 // move first in the container
180                 moveItem(sourceItem, targetItem, MoveLocation.INSIDE);
181                 container.setParent(sourceItemId, targetItemId);
182             }
183         } else {
184             Object parentId = container.getParent(targetItemId);
185             // MGNLUI-4082: In case of moving a node to root whose parentId is null, get parent from target item.
186             if (parentId == null && targetItem.getJcrItem() != null) {
187                 try {
188                     parentId = JcrItemUtil.getItemId(targetItem.getJcrItem().getParent());
189                 } catch (RepositoryException e) {
190                     log.info("Cannot execute drag and drop node " + sourceItem.getJcrItem() + " to " + targetItem.getJcrItem(), e);
191                 }
192             }
193             if (location == VerticalDropLocation.TOP) {
194                 if (parentId != null && constraint.allowedBefore(sourceItem, targetItem)) {
195                     // move first in the container
196                     moveItem(sourceItem, targetItem, MoveLocation.BEFORE);
197                     container.setParent(sourceItemId, parentId);
198                 }
199             } else if (location == VerticalDropLocation.BOTTOM) {
200                 if (parentId != null && constraint.allowedAfter(sourceItem, targetItem)) {
201                     moveItem(sourceItem, targetItem, MoveLocation.AFTER);
202                     container.setParent(sourceItemId, parentId);
203                 }
204             }
205         }
206     }
207 
208     /**
209      * Create a serverSide {@link AcceptCriterion} based on the {@link DropConstraint} implementation.
210      */
211     private void createAcceptCriterion() {
212         serverSideCriterion = new ServerSideCriterion() {
213 
214             @Override
215             public boolean accept(DragAndDropEvent dragEvent) {
216                 boolean res = true;
217                 Iterator<Object> selected = getItemIdsToMove(dragEvent).iterator();
218                 while (selected.hasNext()) {
219                     Object sourceItemId = selected.next();
220                     HierarchicalJcrContainer container = (HierarchicalJcrContainer) tree.getContainerDataSource();
221                     JcrItemAdapter sourceItem = (JcrItemAdapter) container.getItem(sourceItemId);
222                     res &= constraint.allowedToMove(sourceItem);
223                 }
224                 return res;
225             }
226         };
227 
228     }
229 
230     /**
231      * Performs check for moving items. Evaluates true when first node is of type Node.
232      */
233     public boolean basicMoveCheck(Item source, Item target) {
234         try {
235             // Target must be node, to allow moving in
236             if (!target.isNode()) {
237                 return false;
238             }
239             // Source and origin are the same... do nothing
240             if (target.getPath().equals(source.getPath()) && target.getSession().getWorkspace().getName().equals(source.getSession().getWorkspace().getName())) {
241                 return false;
242             }
243 
244             // A property can't be moved into its parent or node which already has that property name.
245             Node targetNode = (Node) target;
246             if (!source.isNode() && (NodeUtil.isSame(targetNode, source.getParent()) || targetNode.hasProperty(source.getName()))) {
247                 return false;
248             }
249 
250             return true;
251         } catch (RepositoryException re) {
252             log.debug("Cannot determine whether drag and drop is possible due to: {}", re.getMessage(), re);
253             return false;
254         }
255     }
256 
257     /**
258      * Performs move of the source node into target node or next to target node depending on the value of MoveLocation.
259      * Move is performed in session-write mode and as such requires explicit call to session.save() after performing the operation.
260      */
261     public void moveNode(Node nodeToMove, Node newParent, MoveLocation location) throws RepositoryException {
262         if (!basicMoveCheck(nodeToMove, newParent)) {
263             return;
264         }
265         switch (location) {
266         case INSIDE:
267             String newPath = combinePathAndName(newParent.getPath(), nodeToMove.getName());
268             nodeToMove.getSession().move(nodeToMove.getPath(), newPath);
269             break;
270         case BEFORE:
271             moveNodeBefore(nodeToMove, newParent);
272             break;
273         case AFTER:
274             moveNodeAfter(nodeToMove, newParent);
275             break;
276         }
277     }
278 
279     /**
280      * Performs move of the source node or property into target node or next to target node depending on the value of MoveLocation.
281      * This method will persist move by calling session.save() explicitly. And will return true/false depending on whether move was successful or not.
282      */
283     @Override
284     public boolean moveItem(com.vaadin.v7.data.Item source, com.vaadin.v7.data.Item target, MoveLocation location) {
285         Item sourceItem = ((JcrItemAdapter) source).getJcrItem();
286         Item targetItem = ((JcrItemAdapter) target).getJcrItem();
287         if (!basicMoveCheck(sourceItem, targetItem)) {
288             return false;
289         }
290         try {
291             final Node targetNode = (Node) targetItem;
292             if (sourceItem.isNode()) {
293                 moveNode((Node) sourceItem, targetNode, location);
294             } else if (location == MoveLocation.INSIDE) { // Only allow moving property INSIDE a node
295                 NodeUtil.moveProperty((Property) sourceItem, targetNode);
296             } else {
297                 return false;
298             }
299             sourceItem.getSession().save();
300             return true;
301         } catch (RepositoryException re) {
302             log.debug("Cannot execute drag and drop action", re);
303             return false;
304         }
305     }
306 
307     @Override
308     public DropHandler asDropHandler() {
309         return this;
310     }
311 }