source: josm/trunk/src/org/openstreetmap/josm/command/ChangePropertyCommand.java@ 17379

Last change on this file since 17379 was 17358, checked in by GerdP, 3 years ago

see #19885: memory leak with "temporary" objects in validator and actions

  • (hopefully) fix memory leaks in complex actions
  • handle complex cases with presets and RelationEditor

I hope these changes don't break plugins which extend or overwrite RelationEditor

  • Property svn:eol-style set to native
File size: 12.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.command;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.util.Collection;
9import java.util.Collections;
10import java.util.HashMap;
11import java.util.LinkedList;
12import java.util.List;
13import java.util.Map;
14import java.util.Map.Entry;
15import java.util.NoSuchElementException;
16import java.util.Objects;
17import java.util.stream.Collectors;
18
19import javax.swing.Icon;
20
21import org.openstreetmap.josm.data.osm.DataSet;
22import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
23import org.openstreetmap.josm.data.osm.OsmPrimitive;
24import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
25import org.openstreetmap.josm.data.osm.Tagged;
26import org.openstreetmap.josm.tools.I18n;
27import org.openstreetmap.josm.tools.ImageProvider;
28
29/**
30 * Command that manipulate the key/value structure of several objects. Manages deletion,
31 * adding and modify of values and keys.
32 *
33 * @author imi
34 * @since 24
35 */
36public class ChangePropertyCommand extends Command {
37
38 static final class OsmPseudoCommand implements PseudoCommand {
39 private final OsmPrimitive osm;
40
41 OsmPseudoCommand(OsmPrimitive osm) {
42 this.osm = osm;
43 }
44
45 @Override
46 public String getDescriptionText() {
47 return osm.getDisplayName(DefaultNameFormatter.getInstance());
48 }
49
50 @Override
51 public Icon getDescriptionIcon() {
52 return ImageProvider.get(osm.getDisplayType());
53 }
54
55 @Override
56 public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
57 return Collections.singleton(osm);
58 }
59 }
60
61 /**
62 * All primitives that are affected with this command.
63 */
64 private final List<OsmPrimitive> objects = new LinkedList<>();
65
66 /**
67 * Key and value pairs. If value is <code>null</code>, delete all key references with the given
68 * key. Otherwise, change the tags of all objects to the given value or create keys of
69 * those objects that do not have the key yet.
70 */
71 private final Map<String, String> tags;
72
73 /**
74 * Creates a command to change multiple tags of multiple objects
75 *
76 * @param ds The target data set. Must not be {@code null}
77 * @param objects the objects to modify. Must not be empty
78 * @param tags the tags to set. Caller must make sure that the tas are not changed once the command was executed.
79 * @since 12726
80 */
81 public ChangePropertyCommand(DataSet ds, Collection<? extends OsmPrimitive> objects, Map<String, String> tags) {
82 super(ds);
83 this.tags = tags;
84 init(objects);
85 }
86
87 /**
88 * Creates a command to change multiple tags of multiple objects
89 *
90 * @param objects the objects to modify. Must not be empty, and objects must belong to a data set
91 * @param tags the tags to set. Caller must make sure that the tas are not changed once the command was executed.
92 * @throws NullPointerException if objects is null or contain null item
93 * @throws NoSuchElementException if objects is empty
94 */
95 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, Map<String, String> tags) {
96 this(objects.iterator().next().getDataSet(), objects, tags);
97 }
98
99 /**
100 * Creates a command to change one tag of multiple objects
101 *
102 * @param objects the objects to modify. Must not be empty, and objects must belong to a data set
103 * @param key the key of the tag to set
104 * @param value the value of the key to set
105 * @throws NullPointerException if objects is null or contain null item
106 * @throws NoSuchElementException if objects is empty
107 */
108 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, String key, String value) {
109 super(objects.iterator().next().getDataSet());
110 this.tags = Collections.singletonMap(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(Collections.singleton(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 = this.tags.entrySet().stream()
235 .allMatch(tag -> tag.getValue() == null || tag.getValue().isEmpty());
236
237 if (allNull) {
238 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */
239 text = trn("Deleted {0} tags for {1} object", "Deleted {0} tags for {1} objects", objects.size(), tags.size(), objects.size());
240 } else {
241 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */
242 text = trn("Set {0} tags for {1} object", "Set {0} tags for {1} objects", objects.size(), tags.size(), objects.size());
243 }
244 }
245 return text;
246 }
247
248 @Override
249 public Icon getDescriptionIcon() {
250 return ImageProvider.get("dialogs", "propertiesdialog", ImageProvider.ImageSizes.SMALLICON);
251 }
252
253 @Override
254 public Collection<PseudoCommand> getChildren() {
255 if (objects.size() == 1)
256 return null;
257 return objects.stream().map(OsmPseudoCommand::new).collect(Collectors.toList());
258 }
259
260 /**
261 * Returns the number of objects that will effectively be modified, before the command is executed.
262 * @return the number of objects that will effectively be modified (can be 0)
263 * @see Command#getParticipatingPrimitives()
264 * @since 8945
265 */
266 public final int getObjectsNumber() {
267 return objects.size();
268 }
269
270 /**
271 * Returns the tags to set (key/value pairs).
272 * @return the tags to set (key/value pairs)
273 */
274 public Map<String, String> getTags() {
275 return Collections.unmodifiableMap(tags);
276 }
277
278 @Override
279 public int hashCode() {
280 return Objects.hash(super.hashCode(), objects, tags);
281 }
282
283 @Override
284 public boolean equals(Object obj) {
285 if (this == obj) return true;
286 if (obj == null || getClass() != obj.getClass()) return false;
287 if (!super.equals(obj)) return false;
288 ChangePropertyCommand that = (ChangePropertyCommand) obj;
289 return Objects.equals(objects, that.objects) &&
290 Objects.equals(tags, that.tags);
291 }
292
293 /**
294 * Calculate the {@link ChangePropertyCommand} that is needed to change the tags in source to be equal to those in target.
295 * @param source the source primitive
296 * @param target the target primitive
297 * @return null if no changes are needed, else a {@link ChangePropertyCommand}
298 * @since 17357
299 */
300 public static Command build(OsmPrimitive source, Tagged target) {
301 Map<String, String> changedTags = new HashMap<>();
302 // find tags which have to be changed or removed
303 for (Entry<String, String> tag : source.getKeys().entrySet()) {
304 String key = tag.getKey();
305 String val = target.get(key);
306 if (!tag.getValue().equals(val))
307 changedTags.put(key, val); // null or a different value
308 }
309 // find tags which exist only in target, they have to be added
310 for (Entry<String, String> tag : target.getKeys().entrySet()) {
311 String key = tag.getKey();
312 if (!source.hasTag(key))
313 changedTags.put(key, tag.getValue());
314 }
315 if (changedTags.isEmpty())
316 return null;
317 if (changedTags.size() == 1) {
318 Entry<String, String> tag = changedTags.entrySet().iterator().next();
319 return new ChangePropertyCommand(Collections.singleton(source), tag.getKey(), tag.getValue());
320 }
321 return new ChangePropertyCommand(Collections.singleton(source), new HashMap<>(changedTags));
322 }
323}
Note: See TracBrowser for help on using the repository browser.