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;
010import java.util.stream.Collectors;
011import java.util.stream.Stream;
012
013import javax.swing.Icon;
014
015import org.openstreetmap.josm.data.osm.DataSet;
016import org.openstreetmap.josm.data.osm.OsmPrimitive;
017import org.openstreetmap.josm.tools.ImageProvider;
018import org.openstreetmap.josm.tools.Utils;
019import org.openstreetmap.josm.tools.bugreport.ReportedException;
020
021/**
022 * A command consisting of a sequence of other commands. Executes the other commands
023 * and undo them in reverse order.
024 * @author imi
025 * @since 31
026 */
027public class SequenceCommand extends Command {
028
029    /** The command sequence to be executed. */
030    private Command[] sequence;
031    private boolean sequenceComplete;
032    private final String name;
033    /** Determines if the sequence execution should continue after one of its commands fails. */
034    protected final boolean continueOnError;
035
036    /**
037     * Create the command by specifying the list of commands to execute.
038     * @param ds The target data set. Must not be {@code null}
039     * @param name The description text
040     * @param sequenz The sequence that should be executed
041     * @param continueOnError Determines if the sequence execution should continue after one of its commands fails
042     * @since 12726
043     */
044    public SequenceCommand(DataSet ds, String name, Collection<Command> sequenz, boolean continueOnError) {
045        super(ds);
046        this.name = name;
047        this.sequence = sequenz.toArray(new Command[0]);
048        this.continueOnError = continueOnError;
049    }
050
051    /**
052     * Create the command by specifying the list of commands to execute.
053     * @param name The description text
054     * @param sequenz The sequence that should be executed. Must not be null or empty
055     * @param continueOnError Determines if the sequence execution should continue after one of its commands fails
056     * @since 11874
057     */
058    public SequenceCommand(String name, Collection<Command> sequenz, boolean continueOnError) {
059        this(sequenz.iterator().next().getAffectedDataSet(), name, sequenz, continueOnError);
060    }
061
062    /**
063     * Create the command by specifying the list of commands to execute.
064     * @param name The description text
065     * @param sequenz The sequence that should be executed.
066     */
067    public SequenceCommand(String name, Collection<Command> sequenz) {
068        this(name, sequenz, false);
069    }
070
071    /**
072     * Convenient constructor, if the commands are known at compile time.
073     * @param name The description text
074     * @param sequenz The sequence that should be executed.
075     */
076    public SequenceCommand(String name, Command... sequenz) {
077        this(name, Arrays.asList(sequenz));
078    }
079
080    /**
081     * Convenient constructor, if the commands are known at compile time.
082     * @param name The description text to be used for the sequence command, if one is created.
083     * @param sequenz The sequence that should be executed.
084     * @return Either a SequenceCommand, or the only command in the potential sequence
085     * @since 16573
086     */
087    public static Command wrapIfNeeded(String name, Command... sequenz) {
088        if (sequenz.length == 1) {
089            return sequenz[0];
090        }
091        return new SequenceCommand(name, sequenz);
092    }
093
094    /**
095     * Convenient constructor, if the commands are known at compile time.
096     * @param name The description text to be used for the sequence command, if one is created.
097     * @param sequenz The sequence that should be executed.
098     * @return Either a SequenceCommand, or the only command in the potential sequence
099     * @since 16573
100     */
101    public static Command wrapIfNeeded(String name, Collection<Command> sequenz) {
102        if (sequenz.size() == 1) {
103            return sequenz.iterator().next();
104        }
105        return new SequenceCommand(name, sequenz);
106    }
107
108    @Override public boolean executeCommand() {
109        for (int i = 0; i < sequence.length; i++) {
110            boolean result;
111            try {
112                result = sequence[i].executeCommand();
113            } catch (AssertionError | Exception e) {
114                throw createReportedException(e, i);
115            }
116            if (!result && !continueOnError) {
117                undoCommands(i-1);
118                return false;
119            }
120        }
121        sequenceComplete = true;
122        return true;
123    }
124
125    /**
126     * Returns the last command.
127     * @return The last command, or {@code null} if the sequence is empty.
128     */
129    public Command getLastCommand() {
130        if (sequence.length == 0)
131            return null;
132        return sequence[sequence.length-1];
133    }
134
135    protected final void undoCommands(int start) {
136        for (int i = start; i >= 0; --i) {
137            try {
138                sequence[i].undoCommand();
139            } catch (AssertionError | Exception e) {
140                throw createReportedException(e, i);
141            }
142        }
143    }
144
145    private ReportedException createReportedException(Throwable e, int i) {
146        ReportedException exception = new ReportedException(e);
147        exception.startSection("sequence_information");
148        exception.put("sequence_name", getDescriptionText());
149        exception.put("sequence_command", sequence[i].getDescriptionText());
150        exception.put("sequence_index", i);
151        exception.put("sequence_commands", Stream.of(sequence)
152                .map(o -> o.getClass().getCanonicalName())
153                .map(String::valueOf)
154                .collect(Collectors.joining(";", "[", "]")));
155        exception.put("sequence_commands_descriptions", Stream.of(sequence)
156                .map(Command::getDescriptionText)
157                .collect(Collectors.joining(";", "[", "]")));
158        return exception;
159    }
160
161    @Override
162    public void undoCommand() {
163        // We probably aborted this halfway though the execution sequence because of a sub-command error.
164        // We already undid the sub-commands.
165        if (!sequenceComplete)
166            return;
167        undoCommands(sequence.length-1);
168    }
169
170    @Override
171    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
172        for (Command c : sequence) {
173            c.fillModifiedData(modified, deleted, added);
174        }
175    }
176
177    @Override
178    public String getDescriptionText() {
179        return tr("Sequence: {0}", name);
180    }
181
182    /**
183     * Returns the command name used in description text.
184     * @return the command name
185     * @since 14283
186     */
187    public final String getName() {
188        return name;
189    }
190
191    @Override
192    public Icon getDescriptionIcon() {
193        return ImageProvider.get("data", "sequence");
194    }
195
196    @Override
197    public Collection<PseudoCommand> getChildren() {
198        return Arrays.<PseudoCommand>asList(sequence);
199    }
200
201    @Override
202    public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
203        Collection<OsmPrimitive> prims = new HashSet<>();
204        for (Command c : sequence) {
205            prims.addAll(c.getParticipatingPrimitives());
206        }
207        return prims;
208    }
209
210    protected final void setSequence(Command... sequence) {
211        this.sequence = Utils.copyArray(sequence);
212    }
213
214    protected final void setSequenceComplete(boolean sequenceComplete) {
215        this.sequenceComplete = sequenceComplete;
216    }
217
218    @Override
219    public int hashCode() {
220        return Objects.hash(super.hashCode(), Arrays.hashCode(sequence), sequenceComplete, name, continueOnError);
221    }
222
223    @Override
224    public boolean equals(Object obj) {
225        if (this == obj) return true;
226        if (obj == null || getClass() != obj.getClass()) return false;
227        if (!super.equals(obj)) return false;
228        SequenceCommand that = (SequenceCommand) obj;
229        return sequenceComplete == that.sequenceComplete &&
230                continueOnError == that.continueOnError &&
231                Arrays.equals(sequence, that.sequence) &&
232                Objects.equals(name, that.name);
233    }
234}