001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions.upload;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.Collection;
007import java.util.Collections;
008import java.util.HashMap;
009import java.util.LinkedList;
010import java.util.List;
011import java.util.Map;
012import java.util.Map.Entry;
013
014import org.openstreetmap.josm.Main;
015import org.openstreetmap.josm.command.ChangePropertyCommand;
016import org.openstreetmap.josm.command.Command;
017import org.openstreetmap.josm.command.SequenceCommand;
018import org.openstreetmap.josm.data.APIDataSet;
019import org.openstreetmap.josm.data.osm.OsmPrimitive;
020import org.openstreetmap.josm.data.osm.Relation;
021import org.openstreetmap.josm.data.osm.Tag;
022
023/**
024 * Fixes defective data entries for all modified objects before upload
025 * @since 5621
026 */
027public class FixDataHook implements UploadHook {
028
029    /**
030     * List of checks to run on data
031     */
032    private final List<FixData> deprecated = new LinkedList<>();
033
034    /**
035     * Constructor for data initialization
036     */
037    public FixDataHook() {
038        // CHECKSTYLE.OFF: SingleSpaceSeparator
039        deprecated.add(new FixDataSpace());
040        deprecated.add(new FixDataKey("color",            "colour"));
041        deprecated.add(new FixDataTag("highway", "ford",  "ford",    "yes"));
042        deprecated.add(new FixDataTag("oneway",  "false", "oneway",  "no"));
043        deprecated.add(new FixDataTag("oneway",  "0",     "oneway",  "no"));
044        deprecated.add(new FixDataTag("oneway",  "true",  "oneway",  "yes"));
045        deprecated.add(new FixDataTag("oneway",  "1",     "oneway",  "yes"));
046        deprecated.add(new FixDataTag("highway", "stile", "barrier", "stile"));
047        // CHECKSTYLE.ON: SingleSpaceSeparator
048        deprecated.add((keys, osm) -> {
049            if (osm instanceof Relation && "multipolygon".equals(keys.get("type")) && "administrative".equals(keys.get("boundary"))) {
050                keys.put("type", "boundary");
051                return true;
052            }
053            return false;
054        });
055    }
056
057    /**
058     * Common set of commands for data fixing
059     * @since 10600 (functional interface)
060     */
061    @FunctionalInterface
062    public interface FixData {
063        /**
064         * Checks if data needs to be fixed and change keys
065         *
066         * @param keys list of keys to be modified
067         * @param osm the object for type validation, don't use keys of it!
068         * @return <code>true</code> if keys have been modified
069         */
070        boolean fixKeys(Map<String, String> keys, OsmPrimitive osm);
071    }
072
073    /**
074     * Data fix to remove spaces at begin or end of tags
075     */
076    public static class FixDataSpace implements FixData {
077        @Override
078        public boolean fixKeys(Map<String, String> keys, OsmPrimitive osm) {
079            Map<String, String> newKeys = new HashMap<>(keys);
080            for (Entry<String, String> e : keys.entrySet()) {
081                String v = Tag.removeWhiteSpaces(e.getValue());
082                String k = Tag.removeWhiteSpaces(e.getKey());
083                boolean drop = k.isEmpty() || v.isEmpty();
084                if (!e.getKey().equals(k)) {
085                    if (drop || !keys.containsKey(k)) {
086                        newKeys.put(e.getKey(), null);
087                        if (!drop)
088                            newKeys.put(k, v);
089                    }
090                } else if (!e.getValue().equals(v)) {
091                    newKeys.put(k, v.isEmpty() ? null : v);
092                } else if (drop) {
093                    newKeys.put(e.getKey(), null);
094                }
095            }
096            boolean changed = !keys.equals(newKeys);
097            if (changed) {
098                keys.clear();
099                keys.putAll(newKeys);
100            }
101            return changed;
102        }
103    }
104
105    /**
106     * Data fix to cleanup wrong spelled keys
107     */
108    public static class FixDataKey implements FixData {
109        /** key of wrong data */
110        private final String oldKey;
111        /** key of correct data */
112        private final String newKey;
113
114        /**
115         * Setup key check for wrong spelled keys
116         *
117         * @param oldKey wrong spelled key
118         * @param newKey correct replacement
119         */
120        public FixDataKey(String oldKey, String newKey) {
121            this.oldKey = oldKey;
122            this.newKey = newKey;
123        }
124
125        @Override
126        public boolean fixKeys(Map<String, String> keys, OsmPrimitive osm) {
127            if (keys.containsKey(oldKey) && !keys.containsKey(newKey)) {
128                keys.put(newKey, keys.get(oldKey));
129                keys.put(oldKey, null);
130                return true;
131            } else if (keys.containsKey(oldKey) && keys.containsKey(newKey) && keys.get(oldKey).equals(keys.get(newKey))) {
132                keys.put(oldKey, null);
133                return true;
134            }
135            return false;
136        }
137    }
138
139    /**
140     * Data fix to cleanup wrong spelled tags
141     */
142    public static class FixDataTag implements FixData {
143        /** key of wrong data */
144        private final String oldKey;
145        /** value of wrong data */
146        private final String oldValue;
147        /** key of correct data */
148        private final String newKey;
149        /** value of correct data */
150        private final String newValue;
151
152        /**
153         * Setup key check for wrong spelled keys
154         *
155         * @param oldKey wrong or old key
156         * @param oldValue wrong or old value
157         * @param newKey correct key replacement
158         * @param newValue correct value replacement
159         */
160        public FixDataTag(String oldKey, String oldValue, String newKey, String newValue) {
161            this.oldKey = oldKey;
162            this.oldValue = oldValue;
163            this.newKey = newKey;
164            this.newValue = newValue;
165        }
166
167        @Override
168        public boolean fixKeys(Map<String, String> keys, OsmPrimitive osm) {
169            if (oldValue.equals(keys.get(oldKey)) && (newKey.equals(oldKey)
170            || !keys.containsKey(newKey) || keys.get(newKey).equals(newValue))) {
171                keys.put(newKey, newValue);
172                if (!newKey.equals(oldKey))
173                    keys.put(oldKey, null);
174                return true;
175            }
176            return false;
177        }
178    }
179
180    /**
181     * Checks the upload for deprecated or wrong tags.
182     * @param apiDataSet the data to upload
183     */
184    @Override
185    public boolean checkUpload(APIDataSet apiDataSet) {
186        if (!Main.pref.getBoolean("fix.data.on.upload", true))
187            return true;
188
189        List<OsmPrimitive> objectsToUpload = apiDataSet.getPrimitives();
190        Collection<Command> cmds = new LinkedList<>();
191
192        for (OsmPrimitive osm : objectsToUpload) {
193            Map<String, String> keys = new HashMap<>(osm.getKeys());
194            if (!keys.isEmpty()) {
195                boolean modified = false;
196                for (FixData fix : deprecated) {
197                    if (fix.fixKeys(keys, osm))
198                        modified = true;
199                }
200                if (modified)
201                    cmds.add(new ChangePropertyCommand(Collections.singleton(osm), keys));
202            }
203        }
204
205        if (!cmds.isEmpty())
206            Main.main.undoRedo.add(new SequenceCommand(tr("Fix deprecated tags"), cmds));
207        return true;
208    }
209}