001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.command; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.util.Arrays; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.HashMap; 012import java.util.LinkedList; 013import java.util.List; 014import java.util.Map; 015import java.util.NoSuchElementException; 016import java.util.Objects; 017import java.util.stream.Collectors; 018 019import javax.swing.Icon; 020 021import org.openstreetmap.josm.data.osm.DataSet; 022import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 023import org.openstreetmap.josm.data.osm.OsmPrimitive; 024import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 025import org.openstreetmap.josm.tools.I18n; 026import org.openstreetmap.josm.tools.ImageProvider; 027 028/** 029 * Command that manipulate the key/value structure of several objects. Manages deletion, 030 * adding and modify of values and keys. 031 * 032 * @author imi 033 * @since 24 034 */ 035public class ChangePropertyCommand extends Command { 036 037 static final class OsmPseudoCommand implements PseudoCommand { 038 private final OsmPrimitive osm; 039 040 OsmPseudoCommand(OsmPrimitive osm) { 041 this.osm = osm; 042 } 043 044 @Override 045 public String getDescriptionText() { 046 return osm.getDisplayName(DefaultNameFormatter.getInstance()); 047 } 048 049 @Override 050 public Icon getDescriptionIcon() { 051 return ImageProvider.get(osm.getDisplayType()); 052 } 053 054 @Override 055 public Collection<? extends OsmPrimitive> getParticipatingPrimitives() { 056 return Collections.singleton(osm); 057 } 058 } 059 060 /** 061 * All primitives that are affected with this command. 062 */ 063 private final List<OsmPrimitive> objects = new LinkedList<>(); 064 065 /** 066 * Key and value pairs. If value is <code>null</code>, delete all key references with the given 067 * key. Otherwise, change the tags of all objects to the given value or create keys of 068 * those objects that do not have the key yet. 069 */ 070 private final Map<String, String> tags; 071 072 /** 073 * Creates a command to change multiple tags of multiple objects 074 * 075 * @param ds The target data set. Must not be {@code null} 076 * @param objects the objects to modify. Must not be empty 077 * @param tags the tags to set 078 * @since 12726 079 */ 080 public ChangePropertyCommand(DataSet ds, Collection<? extends OsmPrimitive> objects, Map<String, String> tags) { 081 super(ds); 082 this.tags = tags; 083 init(objects); 084 } 085 086 /** 087 * Creates a command to change multiple tags of multiple objects 088 * 089 * @param objects the objects to modify. Must not be empty, and objects must belong to a data set 090 * @param tags the tags to set 091 * @throws NullPointerException if objects is null or contain null item 092 * @throws NoSuchElementException if objects is empty 093 */ 094 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, Map<String, String> tags) { 095 this(objects.iterator().next().getDataSet(), objects, tags); 096 } 097 098 /** 099 * Creates a command to change one tag of multiple objects 100 * 101 * @param objects the objects to modify. Must not be empty, and objects must belong to a data set 102 * @param key the key of the tag to set 103 * @param value the value of the key to set 104 * @throws NullPointerException if objects is null or contain null item 105 * @throws NoSuchElementException if objects is empty 106 */ 107 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, String key, String value) { 108 super(objects.iterator().next().getDataSet()); 109 this.tags = new HashMap<>(1); 110 this.tags.put(key, value); 111 init(objects); 112 } 113 114 /** 115 * Creates a command to change one tag of one object 116 * 117 * @param object the object to modify. Must belong to a data set 118 * @param key the key of the tag to set 119 * @param value the value of the key to set 120 * @throws NullPointerException if object is null 121 */ 122 public ChangePropertyCommand(OsmPrimitive object, String key, String value) { 123 this(Arrays.asList(object), key, value); 124 } 125 126 /** 127 * Initialize the instance by finding what objects will be modified 128 * 129 * @param objects the objects to (possibly) modify 130 */ 131 private void init(Collection<? extends OsmPrimitive> objects) { 132 // determine what objects will be modified 133 for (OsmPrimitive osm : objects) { 134 boolean modified = false; 135 136 // loop over all tags 137 for (Map.Entry<String, String> tag : this.tags.entrySet()) { 138 String oldVal = osm.get(tag.getKey()); 139 String newVal = tag.getValue(); 140 141 if (newVal == null || newVal.isEmpty()) { 142 if (oldVal != null) { 143 // new value is null and tag exists (will delete tag) 144 modified = true; 145 break; 146 } 147 } else if (oldVal == null || !newVal.equals(oldVal)) { 148 // new value is not null and is different from current value 149 modified = true; 150 break; 151 } 152 } 153 if (modified) 154 this.objects.add(osm); 155 } 156 } 157 158 @Override 159 public boolean executeCommand() { 160 if (objects.isEmpty()) 161 return true; 162 final DataSet dataSet = objects.get(0).getDataSet(); 163 if (dataSet != null) { 164 dataSet.beginUpdate(); 165 } 166 try { 167 super.executeCommand(); // save old 168 169 for (OsmPrimitive osm : objects) { 170 // loop over all tags 171 for (Map.Entry<String, String> tag : this.tags.entrySet()) { 172 String oldVal = osm.get(tag.getKey()); 173 String newVal = tag.getValue(); 174 175 if (newVal == null || newVal.isEmpty()) { 176 if (oldVal != null) 177 osm.remove(tag.getKey()); 178 } else if (oldVal == null || !newVal.equals(oldVal)) 179 osm.put(tag.getKey(), newVal); 180 } 181 // init() only keeps modified primitives. Therefore the modified 182 // bit can be set without further checks. 183 osm.setModified(true); 184 } 185 return true; 186 } finally { 187 if (dataSet != null) { 188 dataSet.endUpdate(); 189 } 190 } 191 } 192 193 @Override 194 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) { 195 modified.addAll(objects); 196 } 197 198 @Override 199 public String getDescriptionText() { 200 @I18n.QuirkyPluralString 201 final String text; 202 if (objects.size() == 1 && tags.size() == 1) { 203 OsmPrimitive primitive = objects.get(0); 204 String msg; 205 Map.Entry<String, String> entry = tags.entrySet().iterator().next(); 206 if (entry.getValue() == null || entry.getValue().isEmpty()) { 207 switch(OsmPrimitiveType.from(primitive)) { 208 case NODE: msg = marktr("Remove \"{0}\" for node ''{1}''"); break; 209 case WAY: msg = marktr("Remove \"{0}\" for way ''{1}''"); break; 210 case RELATION: msg = marktr("Remove \"{0}\" for relation ''{1}''"); break; 211 default: throw new AssertionError(); 212 } 213 text = tr(msg, entry.getKey(), primitive.getDisplayName(DefaultNameFormatter.getInstance())); 214 } else { 215 switch(OsmPrimitiveType.from(primitive)) { 216 case NODE: msg = marktr("Set {0}={1} for node ''{2}''"); break; 217 case WAY: msg = marktr("Set {0}={1} for way ''{2}''"); break; 218 case RELATION: msg = marktr("Set {0}={1} for relation ''{2}''"); break; 219 default: throw new AssertionError(); 220 } 221 text = tr(msg, entry.getKey(), entry.getValue(), primitive.getDisplayName(DefaultNameFormatter.getInstance())); 222 } 223 } else if (objects.size() > 1 && tags.size() == 1) { 224 Map.Entry<String, String> entry = tags.entrySet().iterator().next(); 225 if (entry.getValue() == null || entry.getValue().isEmpty()) { 226 /* I18n: plural form for objects, but value < 2 not possible! */ 227 text = trn("Remove \"{0}\" for {1} object", "Remove \"{0}\" for {1} objects", objects.size(), entry.getKey(), objects.size()); 228 } else { 229 /* I18n: plural form for objects, but value < 2 not possible! */ 230 text = trn("Set {0}={1} for {2} object", "Set {0}={1} for {2} objects", 231 objects.size(), entry.getKey(), entry.getValue(), objects.size()); 232 } 233 } else { 234 boolean allnull = true; 235 for (Map.Entry<String, String> tag : this.tags.entrySet()) { 236 if (tag.getValue() != null && !tag.getValue().isEmpty()) { 237 allnull = false; 238 break; 239 } 240 } 241 242 if (allnull) { 243 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */ 244 text = trn("Deleted {0} tags for {1} object", "Deleted {0} tags for {1} objects", objects.size(), tags.size(), objects.size()); 245 } else { 246 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */ 247 text = trn("Set {0} tags for {1} object", "Set {0} tags for {1} objects", objects.size(), tags.size(), objects.size()); 248 } 249 } 250 return text; 251 } 252 253 @Override 254 public Icon getDescriptionIcon() { 255 return ImageProvider.get("data", "key"); 256 } 257 258 @Override 259 public Collection<PseudoCommand> getChildren() { 260 if (objects.size() == 1) 261 return null; 262 return objects.stream().map(OsmPseudoCommand::new).collect(Collectors.toList()); 263 } 264 265 /** 266 * Returns the number of objects that will effectively be modified, before the command is executed. 267 * @return the number of objects that will effectively be modified (can be 0) 268 * @see Command#getParticipatingPrimitives() 269 * @since 8945 270 */ 271 public final int getObjectsNumber() { 272 return objects.size(); 273 } 274 275 /** 276 * Returns the tags to set (key/value pairs). 277 * @return the tags to set (key/value pairs) 278 */ 279 public Map<String, String> getTags() { 280 return Collections.unmodifiableMap(tags); 281 } 282 283 @Override 284 public int hashCode() { 285 return Objects.hash(super.hashCode(), objects, tags); 286 } 287 288 @Override 289 public boolean equals(Object obj) { 290 if (this == obj) return true; 291 if (obj == null || getClass() != obj.getClass()) return false; 292 if (!super.equals(obj)) return false; 293 ChangePropertyCommand that = (ChangePropertyCommand) obj; 294 return Objects.equals(objects, that.objects) && 295 Objects.equals(tags, that.tags); 296 } 297}