001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.command;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.Arrays;
007import java.util.Collection;
008import java.util.HashSet;
009import java.util.Objects;
010
011import javax.swing.Icon;
012
013import org.openstreetmap.josm.data.osm.DataSet;
014import org.openstreetmap.josm.data.osm.OsmPrimitive;
015import org.openstreetmap.josm.tools.ImageProvider;
016import org.openstreetmap.josm.tools.Utils;
017
018/**
019 * A command consisting of a sequence of other commands. Executes the other commands
020 * and undo them in reverse order.
021 * @author imi
022 * @since 31
023 */
024public class SequenceCommand extends Command {
025
026    /** The command sequence to be executed. */
027    private Command[] sequence;
028    private boolean sequenceComplete;
029    private final String name;
030    /** Determines if the sequence execution should continue after one of its commands fails. */
031    protected final boolean continueOnError;
032
033    /**
034     * Create the command by specifying the list of commands to execute.
035     * @param ds The target data set. Must not be {@code null}
036     * @param name The description text
037     * @param sequenz The sequence that should be executed
038     * @param continueOnError Determines if the sequence execution should continue after one of its commands fails
039     * @since 12726
040     */
041    public SequenceCommand(DataSet ds, String name, Collection<Command> sequenz, boolean continueOnError) {
042        super(ds);
043        this.name = name;
044        this.sequence = sequenz.toArray(new Command[0]);
045        this.continueOnError = continueOnError;
046    }
047
048    /**
049     * Create the command by specifying the list of commands to execute.
050     * @param name The description text
051     * @param sequenz The sequence that should be executed. Must not be null or empty
052     * @param continueOnError Determines if the sequence execution should continue after one of its commands fails
053     * @since 11874
054     */
055    public SequenceCommand(String name, Collection<Command> sequenz, boolean continueOnError) {
056        this(sequenz.iterator().next().getAffectedDataSet(), name, sequenz, continueOnError);
057    }
058
059    /**
060     * Create the command by specifying the list of commands to execute.
061     * @param name The description text
062     * @param sequenz The sequence that should be executed.
063     */
064    public SequenceCommand(String name, Collection<Command> sequenz) {
065        this(name, sequenz, false);
066    }
067
068    /**
069     * Convenient constructor, if the commands are known at compile time.
070     * @param name The description text
071     * @param sequenz The sequence that should be executed.
072     */
073    public SequenceCommand(String name, Command... sequenz) {
074        this(name, Arrays.asList(sequenz));
075    }
076
077    @Override public boolean executeCommand() {
078        for (int i = 0; i < sequence.length; i++) {
079            boolean result = sequence[i].executeCommand();
080            if (!result && !continueOnError) {
081                undoCommands(i-1);
082                return false;
083            }
084        }
085        sequenceComplete = true;
086        return true;
087    }
088
089    /**
090     * Returns the last command.
091     * @return The last command, or {@code null} if the sequence is empty.
092     */
093    public Command getLastCommand() {
094        if (sequence.length == 0)
095            return null;
096        return sequence[sequence.length-1];
097    }
098
099    protected final void undoCommands(int start) {
100        for (int i = start; i >= 0; --i) {
101            sequence[i].undoCommand();
102        }
103    }
104
105    @Override
106    public void undoCommand() {
107        // We probably aborted this halfway though the execution sequence because of a sub-command error.
108        // We already undid the sub-commands.
109        if (!sequenceComplete)
110            return;
111        undoCommands(sequence.length-1);
112    }
113
114    @Override
115    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
116        for (Command c : sequence) {
117            c.fillModifiedData(modified, deleted, added);
118        }
119    }
120
121    @Override
122    public String getDescriptionText() {
123        return tr("Sequence: {0}", name);
124    }
125
126    /**
127     * Returns the command name used in description text.
128     * @return the command name
129     * @since 14283
130     */
131    public final String getName() {
132        return name;
133    }
134
135    @Override
136    public Icon getDescriptionIcon() {
137        return ImageProvider.get("data", "sequence");
138    }
139
140    @Override
141    public Collection<PseudoCommand> getChildren() {
142        return Arrays.<PseudoCommand>asList(sequence);
143    }
144
145    @Override
146    public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
147        Collection<OsmPrimitive> prims = new HashSet<>();
148        for (Command c : sequence) {
149            prims.addAll(c.getParticipatingPrimitives());
150        }
151        return prims;
152    }
153
154    protected final void setSequence(Command... sequence) {
155        this.sequence = Utils.copyArray(sequence);
156    }
157
158    protected final void setSequenceComplete(boolean sequenceComplete) {
159        this.sequenceComplete = sequenceComplete;
160    }
161
162    @Override
163    public int hashCode() {
164        return Objects.hash(super.hashCode(), Arrays.hashCode(sequence), sequenceComplete, name, continueOnError);
165    }
166
167    @Override
168    public boolean equals(Object obj) {
169        if (this == obj) return true;
170        if (obj == null || getClass() != obj.getClass()) return false;
171        if (!super.equals(obj)) return false;
172        SequenceCommand that = (SequenceCommand) obj;
173        return sequenceComplete == that.sequenceComplete &&
174                continueOnError == that.continueOnError &&
175                Arrays.equals(sequence, that.sequence) &&
176                Objects.equals(name, that.name);
177    }
178}