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