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

Last change on this file was 19050, checked in by taylor.smock, 7 months ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • Property svn:eol-style set to native
File size: 12.6 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;
28import org.openstreetmap.josm.tools.Utils;
29
30/**
31 * Command that manipulate the key/value structure of several objects. Manages deletion,
32 * adding and modify of values and keys.
33 *
34 * @author imi
35 * @since 24
36 */
37public class ChangePropertyCommand extends Command {
38
39 static final class OsmPseudoCommand implements PseudoCommand {
40 private final OsmPrimitive osm;
41
42 OsmPseudoCommand(OsmPrimitive osm) {
43 this.osm = osm;
44 }
45
46 @Override
47 public String getDescriptionText() {
48 return osm.getDisplayName(DefaultNameFormatter.getInstance());
49 }
50
51 @Override
52 public Icon getDescriptionIcon() {
53 return ImageProvider.get(osm.getDisplayType());
54 }
55
56 @Override
57 public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
58 return Collections.singleton(osm);
59 }
60 }
61
62 /**
63 * All primitives that are affected with this command.
64 */
65 private final List<OsmPrimitive> objects = new LinkedList<>();
66
67 /**
68 * Key and value pairs. If value is <code>null</code>, delete all key references with the given
69 * key. Otherwise, change the tags of all objects to the given value or create keys of
70 * those objects that do not have the key yet.
71 */
72 private final Map<String, String> tags;
73
74 /**
75 * Creates a command to change multiple tags of multiple objects
76 *
77 * @param ds The target data set. Must not be {@code null}
78 * @param objects the objects to modify. Must not be empty
79 * @param tags the tags to set. Caller must make sure that the tas are not changed once the command was executed.
80 * @since 12726
81 */
82 public ChangePropertyCommand(DataSet ds, Collection<? extends OsmPrimitive> objects, Map<String, String> tags) {
83 super(ds);
84 this.tags = tags;
85 init(objects);
86 }
87
88 /**
89 * Creates a command to change multiple tags of multiple objects
90 *
91 * @param objects the objects to modify. Must not be empty, and objects must belong to a data set
92 * @param tags the tags to set. Caller must make sure that the tas are not changed once the command was executed.
93 * @throws NullPointerException if objects is null or contain null item
94 * @throws NoSuchElementException if objects is empty
95 */
96 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, Map<String, String> tags) {
97 this(objects.iterator().next().getDataSet(), objects, tags);
98 }
99
100 /**
101 * Creates a command to change one tag of multiple objects
102 *
103 * @param objects the objects to modify. Must not be empty, and objects must belong to a data set
104 * @param key the key of the tag to set
105 * @param value the value of the key to set
106 * @throws NullPointerException if objects is null or contain null item
107 * @throws NoSuchElementException if objects is empty
108 */
109 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, String key, String value) {
110 super(objects.iterator().next().getDataSet());
111 this.tags = Collections.singletonMap(key, value);
112 init(objects);
113 }
114
115 /**
116 * Creates a command to change one tag of one object
117 *
118 * @param object the object to modify. Must belong to a data set
119 * @param key the key of the tag to set
120 * @param value the value of the key to set
121 * @throws NullPointerException if object is null
122 */
123 public ChangePropertyCommand(OsmPrimitive object, String key, String value) {
124 this(Collections.singleton(object), key, value);
125 }
126
127 /**
128 * Initialize the instance by finding what objects will be modified
129 *
130 * @param objects the objects to (possibly) modify
131 */
132 private void init(Collection<? extends OsmPrimitive> objects) {
133 // determine what objects will be modified
134 for (OsmPrimitive osm : objects) {
135 boolean modified = false;
136
137 // loop over all tags
138 for (Map.Entry<String, String> tag : this.tags.entrySet()) {
139 String oldVal = osm.get(tag.getKey());
140 String newVal = tag.getValue();
141
142 if (Utils.isEmpty(newVal)) {
143 if (oldVal != null) {
144 // new value is null and tag exists (will delete tag)
145 modified = true;
146 break;
147 }
148 } else if (oldVal == null || !newVal.equals(oldVal)) {
149 // new value is not null and is different from current value
150 modified = true;
151 break;
152 }
153 }
154 if (modified)
155 this.objects.add(osm);
156 }
157 }
158
159 @Override
160 public boolean executeCommand() {
161 if (objects.isEmpty())
162 return true;
163 final DataSet dataSet = objects.get(0).getDataSet();
164 if (dataSet != null) {
165 dataSet.beginUpdate();
166 }
167 try {
168 super.executeCommand(); // save old
169
170 for (OsmPrimitive osm : objects) {
171 // loop over all tags
172 for (Map.Entry<String, String> tag : this.tags.entrySet()) {
173 String oldVal = osm.get(tag.getKey());
174 String newVal = tag.getValue();
175
176 if (Utils.isEmpty(newVal)) {
177 if (oldVal != null)
178 osm.remove(tag.getKey());
179 } else if (oldVal == null || !newVal.equals(oldVal))
180 osm.put(tag.getKey(), newVal);
181 }
182 // init() only keeps modified primitives. Therefore the modified
183 // bit can be set without further checks.
184 osm.setModified(true);
185 }
186 return true;
187 } finally {
188 if (dataSet != null) {
189 dataSet.endUpdate();
190 }
191 }
192 }
193
194 @Override
195 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
196 modified.addAll(objects);
197 }
198
199 @Override
200 public String getDescriptionText() {
201 @I18n.QuirkyPluralString
202 final String text;
203 if (objects.size() == 1 && tags.size() == 1) {
204 OsmPrimitive primitive = objects.get(0);
205 String msg;
206 Map.Entry<String, String> entry = tags.entrySet().iterator().next();
207 if (Utils.isEmpty(entry.getValue())) {
208 switch (OsmPrimitiveType.from(primitive)) {
209 case NODE: msg = marktr("Remove \"{0}\" for node ''{1}''"); break;
210 case WAY: msg = marktr("Remove \"{0}\" for way ''{1}''"); break;
211 case RELATION: msg = marktr("Remove \"{0}\" for relation ''{1}''"); break;
212 default: throw new AssertionError();
213 }
214 text = tr(msg, entry.getKey(), primitive.getDisplayName(DefaultNameFormatter.getInstance()));
215 } else {
216 switch (OsmPrimitiveType.from(primitive)) {
217 case NODE: msg = marktr("Set {0}={1} for node ''{2}''"); break;
218 case WAY: msg = marktr("Set {0}={1} for way ''{2}''"); break;
219 case RELATION: msg = marktr("Set {0}={1} for relation ''{2}''"); break;
220 default: throw new AssertionError();
221 }
222 text = tr(msg, entry.getKey(), entry.getValue(), primitive.getDisplayName(DefaultNameFormatter.getInstance()));
223 }
224 } else if (objects.size() > 1 && tags.size() == 1) {
225 Map.Entry<String, String> entry = tags.entrySet().iterator().next();
226 if (Utils.isEmpty(entry.getValue())) {
227 /* I18n: plural form for objects, but value < 2 not possible! */
228 text = trn("Remove \"{0}\" for {1} object", "Remove \"{0}\" for {1} objects", objects.size(), entry.getKey(), objects.size());
229 } else {
230 /* I18n: plural form for objects, but value < 2 not possible! */
231 text = trn("Set {0}={1} for {2} object", "Set {0}={1} for {2} objects",
232 objects.size(), entry.getKey(), entry.getValue(), objects.size());
233 }
234 } else {
235 boolean allNull = this.tags.entrySet().stream()
236 .allMatch(tag -> Utils.isEmpty(tag.getValue()));
237
238 if (allNull) {
239 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */
240 text = trn("Deleted {0} tags for {1} object", "Deleted {0} tags for {1} objects", objects.size(), tags.size(), objects.size());
241 } else {
242 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */
243 text = trn("Set {0} tags for {1} object", "Set {0} tags for {1} objects", objects.size(), tags.size(), objects.size());
244 }
245 }
246 return text;
247 }
248
249 @Override
250 public Icon getDescriptionIcon() {
251 return ImageProvider.get("dialogs", "propertiesdialog", ImageProvider.ImageSizes.SMALLICON);
252 }
253
254 @Override
255 public Collection<PseudoCommand> getChildren() {
256 if (objects.size() == 1)
257 return null;
258 return objects.stream().map(OsmPseudoCommand::new).collect(Collectors.toList());
259 }
260
261 /**
262 * Returns the number of objects that will effectively be modified, before the command is executed.
263 * @return the number of objects that will effectively be modified (can be 0)
264 * @see Command#getParticipatingPrimitives()
265 * @since 8945
266 */
267 public final int getObjectsNumber() {
268 return objects.size();
269 }
270
271 /**
272 * Returns the tags to set (key/value pairs).
273 * @return the tags to set (key/value pairs)
274 */
275 public Map<String, String> getTags() {
276 return Collections.unmodifiableMap(tags);
277 }
278
279 @Override
280 public int hashCode() {
281 return Objects.hash(super.hashCode(), objects, tags);
282 }
283
284 @Override
285 public boolean equals(Object obj) {
286 if (this == obj) return true;
287 if (obj == null || getClass() != obj.getClass()) return false;
288 if (!super.equals(obj)) return false;
289 ChangePropertyCommand that = (ChangePropertyCommand) obj;
290 return Objects.equals(objects, that.objects) &&
291 Objects.equals(tags, that.tags);
292 }
293
294 /**
295 * Calculate the {@link ChangePropertyCommand} that is needed to change the tags in source to be equal to those in target.
296 * @param source the source primitive
297 * @param target the target primitive
298 * @return null if no changes are needed, else a {@link ChangePropertyCommand}
299 * @since 17357
300 */
301 public static Command build(OsmPrimitive source, Tagged target) {
302 Map<String, String> changedTags = new HashMap<>();
303 // find tags which have to be changed or removed
304 for (Entry<String, String> tag : source.getKeys().entrySet()) {
305 String key = tag.getKey();
306 String val = target.get(key);
307 if (!tag.getValue().equals(val))
308 changedTags.put(key, val); // null or a different value
309 }
310 // find tags which exist only in target, they have to be added
311 for (Entry<String, String> tag : target.getKeys().entrySet()) {
312 String key = tag.getKey();
313 if (!source.hasTag(key))
314 changedTags.put(key, tag.getValue());
315 }
316 if (changedTags.isEmpty())
317 return null;
318 if (changedTags.size() == 1) {
319 Entry<String, String> tag = changedTags.entrySet().iterator().next();
320 return new ChangePropertyCommand(Collections.singleton(source), tag.getKey(), tag.getValue());
321 }
322 return new ChangePropertyCommand(Collections.singleton(source), new HashMap<>(changedTags));
323 }
324}
Note: See TracBrowser for help on using the repository browser.