001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors. 
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     *
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.graph.request;
025    
026    import org.jboss.dna.common.util.CheckArg;
027    import org.jboss.dna.common.util.HashCode;
028    import org.jboss.dna.graph.GraphI18n;
029    import org.jboss.dna.graph.Location;
030    import org.jboss.dna.graph.NodeConflictBehavior;
031    import org.jboss.dna.graph.property.Name;
032    import org.jboss.dna.graph.property.Path;
033    
034    /**
035     * Instruction that a branch be moved from one location into another.
036     * 
037     * @author Randall Hauch
038     */
039    public class MoveBranchRequest extends ChangeRequest {
040    
041        private static final long serialVersionUID = 1L;
042    
043        public static final NodeConflictBehavior DEFAULT_CONFLICT_BEHAVIOR = NodeConflictBehavior.APPEND;
044    
045        private final Location from;
046        private final Location into;
047        private final Location before;
048        private final String workspaceName;
049        private final Name desiredNameForNode;
050        private final NodeConflictBehavior conflictBehavior;
051        private Location actualOldLocation;
052        private Location actualNewLocation;
053    
054        /**
055         * Create a request to move a branch from one location into another.
056         * 
057         * @param from the location of the top node in the existing branch that is to be moved
058         * @param into the location of the existing node into which the branch should be moved
059         * @param workspaceName the name of the workspace
060         * @throws IllegalArgumentException if any of the parameters are null
061         */
062        public MoveBranchRequest( Location from,
063                                  Location into,
064                                  String workspaceName ) {
065            this(from, into, null, workspaceName, null, DEFAULT_CONFLICT_BEHAVIOR);
066        }
067    
068        /**
069         * Create a request to move a branch from one location into another.
070         * 
071         * @param from the location of the top node in the existing branch that is to be moved
072         * @param into the location of the existing node into which the branch should be moved
073         * @param workspaceName the name of the workspace
074         * @param newNameForMovedNode the new name for the node being moved, or null if the name of the original should be used
075         * @throws IllegalArgumentException if any of the parameters are null
076         */
077        public MoveBranchRequest( Location from,
078                                  Location into,
079                                  String workspaceName,
080                                  Name newNameForMovedNode ) {
081            this(from, into, null, workspaceName, newNameForMovedNode, DEFAULT_CONFLICT_BEHAVIOR);
082        }
083    
084        /**
085         * Create a request to move a branch from one location into another.
086         * 
087         * @param from the location of the top node in the existing branch that is to be moved
088         * @param into the location of the existing node into which the branch should be moved
089         * @param workspaceName the name of the workspace
090         * @param conflictBehavior the expected behavior if an equivalently-named child already exists at the <code>into</code>
091         *        location
092         * @throws IllegalArgumentException if any of the parameters are null
093         */
094        public MoveBranchRequest( Location from,
095                                  Location into,
096                                  String workspaceName,
097                                  NodeConflictBehavior conflictBehavior ) {
098            this(from, into, null, workspaceName, null, conflictBehavior);
099        }
100    
101        /**
102         * Create a request to move a branch from one location into another.
103         * 
104         * @param from the location of the top node in the existing branch that is to be moved
105         * @param into the location of the existing node into which the branch should be moved
106         * @param before the location of the child of the {@code into} node that the branch should be placed before; null indicates
107         *        that the branch should be the last child of its new parent
108         * @param workspaceName the name of the workspace
109         * @param newNameForMovedNode the new name for the node being moved, or null if the name of the original should be used
110         * @param conflictBehavior the expected behavior if an equivalently-named child already exists at the <code>into</code>
111         *        location
112         * @throws IllegalArgumentException if any of the parameters are null
113         */
114        public MoveBranchRequest( Location from,
115                                  Location into,
116                                  Location before,
117                                  String workspaceName,
118                                  Name newNameForMovedNode,
119                                  NodeConflictBehavior conflictBehavior ) {
120            CheckArg.isNotNull(from, "from");
121            // CheckArg.isNotNull(into, "into");
122            CheckArg.isNotNull(workspaceName, "workspaceName");
123            CheckArg.isNotNull(conflictBehavior, "conflictBehavior");
124            this.from = from;
125            this.into = into;
126            this.before = before;
127            this.workspaceName = workspaceName;
128            this.desiredNameForNode = newNameForMovedNode;
129            this.conflictBehavior = conflictBehavior;
130        }
131    
132        /**
133         * Get the location defining the top of the branch to be moved
134         * 
135         * @return the from location; never null
136         */
137        public Location from() {
138            return from;
139        }
140    
141        /**
142         * Get the location defining the parent where the branch is to be placed
143         * 
144         * @return the to location; never null
145         */
146        public Location into() {
147            return into;
148        }
149    
150        /**
151         * Get the location defining the node before which the branch is to be placed
152         * 
153         * @return the to location; null indicates that the branch should be the last child node of its new parent
154         */
155        public Location before() {
156            return before;
157        }
158    
159        /**
160         * Get the name of the workspace in which the branch exists.
161         * 
162         * @return the name of the workspace containing the branch; never null
163         */
164        public String inWorkspace() {
165            return workspaceName;
166        }
167    
168        /**
169         * Get the name of the copy if it is to be different than that of the original.
170         * 
171         * @return the desired name of the copy, or null if the name of the original is to be used
172         */
173        public Name desiredName() {
174            return desiredNameForNode;
175        }
176    
177        /**
178         * Get the expected behavior when copying the branch and the {@link #into() destination} already has a node with the same
179         * name.
180         * 
181         * @return the behavior specification
182         */
183        public NodeConflictBehavior conflictBehavior() {
184            return conflictBehavior;
185        }
186    
187        /**
188         * {@inheritDoc}
189         * 
190         * @see org.jboss.dna.graph.request.Request#isReadOnly()
191         */
192        @Override
193        public boolean isReadOnly() {
194            return false;
195        }
196    
197        /**
198         * Determine whether this move request can be determined to have no effect.
199         * <p>
200         * A move is known to have no effect when all of the following conditions are true:
201         * <ul>
202         * <li>the {@link #into() into} location has a {@link Location#hasPath() path} but no {@link Location#hasIdProperties()
203         * identification properties};</li>
204         * <li>the {@link #from() from} location has a {@link Location#getPath() path}; and</li>
205         * <li>the {@link #from() from} location's {@link Path#getParent() parent} is the same as the {@link #into() into} location's
206         * path.</li>
207         * </ul>
208         * If all of these conditions are not true, this method returns false.
209         * </p>
210         * 
211         * @return true if this move request really doesn't change the parent of the node, or false if it cannot be determined
212         */
213        public boolean hasNoEffect() {
214            if (into != null && into.hasPath() && into.hasIdProperties() == false && from.hasPath()) {
215                if (!from.getPath().getParent().equals(into.getPath())) return false;
216                if (desiredName() != null && !desiredName().equals(from.getPath().getLastSegment().getName())) return false;
217                if (before != null) return false;
218                return true;
219            }
220            // Can't be determined for certain
221            return false;
222        }
223    
224        /**
225         * Sets the actual and complete location of the node being renamed and its new location. This method must be called when
226         * processing the request, and the actual location must have a {@link Location#getPath() path}.
227         * 
228         * @param oldLocation the actual location of the node before being moved
229         * @param newLocation the actual new location of the node
230         * @throws IllegalArgumentException if the either location is null, if the old location does not represent the
231         *         {@link Location#isSame(Location) same location} as the {@link #from() from location}, if the new location does not
232         *         represent the {@link Location#isSame(Location) same location} as the {@link #into() into location}, or if the
233         *         either location does not have a path
234         * @throws IllegalStateException if the request is frozen
235         */
236        public void setActualLocations( Location oldLocation,
237                                        Location newLocation ) {
238            checkNotFrozen();
239            CheckArg.isNotNull(oldLocation, "oldLocation");
240            CheckArg.isNotNull(newLocation, "newLocation");
241            if (!from.isSame(oldLocation)) { // not same if actual is null
242                throw new IllegalArgumentException(GraphI18n.actualLocationIsNotSameAsInputLocation.text(oldLocation, from));
243            }
244            if (!oldLocation.hasPath()) {
245                throw new IllegalArgumentException(GraphI18n.actualOldLocationMustHavePath.text(oldLocation));
246            }
247            if (!newLocation.hasPath()) {
248                throw new IllegalArgumentException(GraphI18n.actualNewLocationMustHavePath.text(newLocation));
249            }
250            if (into() != null && into().hasPath() && !newLocation.getPath().getParent().isSameAs(into.getPath())) {
251                throw new IllegalArgumentException(GraphI18n.actualLocationIsNotSameAsInputLocation.text(newLocation, into));
252            }
253            Name actualNewName = newLocation.getPath().getLastSegment().getName();
254            Name expectedNewName = desiredName() != null ? desiredName() : oldLocation.getPath().getLastSegment().getName();
255            if (!actualNewName.equals(expectedNewName)) {
256                throw new IllegalArgumentException(GraphI18n.actualLocationIsNotSameAsInputLocation.text(newLocation, into));
257            }
258            this.actualOldLocation = oldLocation;
259            this.actualNewLocation = newLocation;
260        }
261    
262        /**
263         * Get the actual location of the node before being moved.
264         * 
265         * @return the actual location of the node before being moved, or null if the actual location was not set
266         */
267        public Location getActualLocationBefore() {
268            return actualOldLocation;
269        }
270    
271        /**
272         * Get the actual location of the node after being moved.
273         * 
274         * @return the actual location of the node after being moved, or null if the actual location was not set
275         */
276        public Location getActualLocationAfter() {
277            return actualNewLocation;
278        }
279    
280        /**
281         * {@inheritDoc}
282         * 
283         * @see org.jboss.dna.graph.request.ChangeRequest#changes(java.lang.String, org.jboss.dna.graph.property.Path)
284         */
285        @Override
286        public boolean changes( String workspace,
287                                Path path ) {
288            if (this.into() != null) {
289                return this.workspaceName.equals(workspace)
290                       && (into.hasPath() && into.getPath().isAtOrBelow(path) || from.hasPath() && from.getPath().isAtOrBelow(path));
291            }
292            // into or before must be non-null
293            assert before() != null;
294            return this.workspaceName.equals(workspace)
295                   && (before.hasPath() && before.getPath().getParent().isAtOrBelow(path) || from.hasPath()
296                                                                                             && from.getPath().isAtOrBelow(path));
297    
298        }
299    
300        /**
301         * {@inheritDoc}
302         * 
303         * @see org.jboss.dna.graph.request.ChangeRequest#changedLocation()
304         */
305        @Override
306        public Location changedLocation() {
307            return into != null ? into : before;
308        }
309    
310        /**
311         * {@inheritDoc}
312         * 
313         * @see org.jboss.dna.graph.request.ChangeRequest#changedWorkspace()
314         */
315        @Override
316        public String changedWorkspace() {
317            return workspaceName;
318        }
319    
320        /**
321         * {@inheritDoc}
322         * 
323         * @see org.jboss.dna.graph.request.Request#cancel()
324         */
325        @Override
326        public void cancel() {
327            super.cancel();
328            this.actualOldLocation = null;
329            this.actualNewLocation = null;
330        }
331    
332        /**
333         * {@inheritDoc}
334         * 
335         * @see java.lang.Object#hashCode()
336         */
337        @Override
338        public int hashCode() {
339            return HashCode.compute(from, workspaceName, into);
340        }
341    
342        /**
343         * {@inheritDoc}
344         * 
345         * @see java.lang.Object#equals(java.lang.Object)
346         */
347        @Override
348        public boolean equals( Object obj ) {
349            if (obj == this) return true;
350            if (this.getClass().isInstance(obj)) {
351                MoveBranchRequest that = (MoveBranchRequest)obj;
352                if (!this.from().equals(that.from())) return false;
353                if (!this.into().equals(that.into())) return false;
354                if (!this.conflictBehavior().equals(that.conflictBehavior())) return false;
355                if (!this.workspaceName.equals(that.workspaceName)) return false;
356                return true;
357            }
358            return false;
359        }
360    
361        /**
362         * {@inheritDoc}
363         * 
364         * @see java.lang.Object#toString()
365         */
366        @Override
367        public String toString() {
368            if (desiredName() != null) {
369                return "move branch " + from() + " in the \"" + inWorkspace() + "\" workspace "
370                       + (into() == null ? "before " + before() : "into " + into()) + " with name " + desiredName();
371            }
372            return "move branch " + from() + " in the \"" + inWorkspace() + "\" workspace into "
373                   + (into() == null ? "before " + before() : "into " + into());
374        }
375    }