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}