source: josm/trunk/src/org/openstreetmap/josm/command/SequenceCommand.java@ 17335

Last change on this file since 17335 was 16681, checked in by simon04, 4 years ago

fix #19407 - SequenceCommand: catch throwable in sequence command and add more data (patch by taylor.smock, modified)

  • Property svn:eol-style set to native
File size: 8.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.command;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.util.Arrays;
7import java.util.Collection;
8import java.util.HashSet;
9import java.util.Objects;
10import java.util.stream.Collectors;
11import java.util.stream.Stream;
12
13import javax.swing.Icon;
14
15import org.openstreetmap.josm.data.osm.DataSet;
16import org.openstreetmap.josm.data.osm.OsmPrimitive;
17import org.openstreetmap.josm.tools.ImageProvider;
18import org.openstreetmap.josm.tools.Utils;
19import org.openstreetmap.josm.tools.bugreport.ReportedException;
20
21/**
22 * A command consisting of a sequence of other commands. Executes the other commands
23 * and undo them in reverse order.
24 * @author imi
25 * @since 31
26 */
27public class SequenceCommand extends Command {
28
29 /** The command sequence to be executed. */
30 private Command[] sequence;
31 private boolean sequenceComplete;
32 private final String name;
33 /** Determines if the sequence execution should continue after one of its commands fails. */
34 protected final boolean continueOnError;
35
36 /**
37 * Create the command by specifying the list of commands to execute.
38 * @param ds The target data set. Must not be {@code null}
39 * @param name The description text
40 * @param sequenz The sequence that should be executed
41 * @param continueOnError Determines if the sequence execution should continue after one of its commands fails
42 * @since 12726
43 */
44 public SequenceCommand(DataSet ds, String name, Collection<Command> sequenz, boolean continueOnError) {
45 super(ds);
46 this.name = name;
47 this.sequence = sequenz.toArray(new Command[0]);
48 this.continueOnError = continueOnError;
49 }
50
51 /**
52 * Create the command by specifying the list of commands to execute.
53 * @param name The description text
54 * @param sequenz The sequence that should be executed. Must not be null or empty
55 * @param continueOnError Determines if the sequence execution should continue after one of its commands fails
56 * @since 11874
57 */
58 public SequenceCommand(String name, Collection<Command> sequenz, boolean continueOnError) {
59 this(sequenz.iterator().next().getAffectedDataSet(), name, sequenz, continueOnError);
60 }
61
62 /**
63 * Create the command by specifying the list of commands to execute.
64 * @param name The description text
65 * @param sequenz The sequence that should be executed.
66 */
67 public SequenceCommand(String name, Collection<Command> sequenz) {
68 this(name, sequenz, false);
69 }
70
71 /**
72 * Convenient constructor, if the commands are known at compile time.
73 * @param name The description text
74 * @param sequenz The sequence that should be executed.
75 */
76 public SequenceCommand(String name, Command... sequenz) {
77 this(name, Arrays.asList(sequenz));
78 }
79
80 /**
81 * Convenient constructor, if the commands are known at compile time.
82 * @param name The description text to be used for the sequence command, if one is created.
83 * @param sequenz The sequence that should be executed.
84 * @return Either a SequenceCommand, or the only command in the potential sequence
85 * @since 16573
86 */
87 public static Command wrapIfNeeded(String name, Command... sequenz) {
88 if (sequenz.length == 1) {
89 return sequenz[0];
90 }
91 return new SequenceCommand(name, sequenz);
92 }
93
94 /**
95 * Convenient constructor, if the commands are known at compile time.
96 * @param name The description text to be used for the sequence command, if one is created.
97 * @param sequenz The sequence that should be executed.
98 * @return Either a SequenceCommand, or the only command in the potential sequence
99 * @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}
Note: See TracBrowser for help on using the repository browser.