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