001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003import static org.openstreetmap.josm.tools.I18n.tr;
004
005import java.util.Collections;
006import java.util.HashMap;
007import java.util.Iterator;
008import java.util.Map;
009import java.util.Map.Entry;
010import java.util.Set;
011
012import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
013import org.openstreetmap.josm.tools.CheckParameterUtil;
014import org.openstreetmap.josm.tools.Logging;
015
016/**
017 * A ChangesetDataSet holds the content of a changeset. Typically, a primitive is modified only once in a changeset,
018 * but if there are multiple modifications, the first and last are kept. Further intermediate versions are not kept.
019 */
020public class ChangesetDataSet {
021
022    /**
023     * Type of primitive modification.
024     */
025    public enum ChangesetModificationType {
026        /** The primitive has been created */
027        CREATED,
028        /** The primitive has been updated */
029        UPDATED,
030        /** The primitive has been deleted */
031        DELETED
032    }
033
034    /**
035     * An entry in the changeset dataset.
036     */
037    public interface ChangesetDataSetEntry {
038
039        /**
040         * Returns the type of modification.
041         * @return the type of modification
042         */
043        ChangesetModificationType getModificationType();
044
045        /**
046         * Returns the affected history primitive.
047         * @return the affected history primitive
048         */
049        HistoryOsmPrimitive getPrimitive();
050    }
051
052    /** maps an id to either one {@link ChangesetDataSetEntry} or an array of {@link ChangesetDataSetEntry} */
053    private final Map<PrimitiveId, Object> entryMap = new HashMap<>();
054
055    /**
056     * Remembers a history primitive with the given modification type
057     *
058     * @param primitive the primitive. Must not be null.
059     * @param cmt the modification type. Must not be null.
060     * @throws IllegalArgumentException if primitive is null
061     * @throws IllegalArgumentException if cmt is null
062     * @throws IllegalArgumentException if the same primitive was already stored with a higher or equal version
063     */
064    public void put(HistoryOsmPrimitive primitive, ChangesetModificationType cmt) {
065        CheckParameterUtil.ensureParameterNotNull(primitive, "primitive");
066        CheckParameterUtil.ensureParameterNotNull(cmt, "cmt");
067        DefaultChangesetDataSetEntry csEntry = new DefaultChangesetDataSetEntry(cmt, primitive);
068        Object val = entryMap.get(primitive.getPrimitiveId());
069        ChangesetDataSetEntry[] entries;
070        if (val == null) {
071            entryMap.put(primitive.getPrimitiveId(), csEntry);
072            return;
073        }
074        if (val instanceof ChangesetDataSetEntry) {
075            entries = new ChangesetDataSetEntry[2];
076            entries[0] = (ChangesetDataSetEntry) val;
077            if (primitive.getVersion() <= entries[0].getPrimitive().getVersion()) {
078                throw new IllegalArgumentException(
079                        tr("Changeset {0}: Unexpected order of versions for {1}: v{2} is not higher than v{3}",
080                                String.valueOf(primitive.getChangesetId()), primitive.getPrimitiveId(),
081                                primitive.getVersion(), entries[0].getPrimitive().getVersion()));
082            }
083        } else {
084            entries = (ChangesetDataSetEntry[]) val;
085        }
086        if (entries[1] != null) {
087            Logging.info("Changeset {0}: Change of {1} v{2} is replaced by version v{3}",
088                    String.valueOf(primitive.getChangesetId()), primitive.getPrimitiveId(),
089                    entries[1].getPrimitive().getVersion(), primitive.getVersion());
090        }
091        entries[1] = csEntry;
092        entryMap.put(primitive.getPrimitiveId(), entries);
093    }
094
095    /**
096     * Replies true if the changeset content contains the object with primitive <code>id</code>.
097     * @param id the id.
098     * @return true if the changeset content contains the object with primitive <code>id</code>
099     */
100    public boolean contains(PrimitiveId id) {
101        if (id == null) return false;
102        return entryMap.containsKey(id);
103    }
104
105    /**
106     * Replies the last modification type for the object with id <code>id</code>. Replies null, if id is null or
107     * if the object with id <code>id</code> isn't in the changeset content.
108     *
109     * @param id the id
110     * @return the last modification type or null
111     */
112    public ChangesetModificationType getModificationType(PrimitiveId id) {
113        ChangesetDataSetEntry e = getLastEntry(id);
114        return e != null ? e.getModificationType() : null;
115    }
116
117    /**
118     * Replies true if the primitive with id <code>id</code> was created in this
119     * changeset. Replies false, if id is null or not in the dataset.
120     *
121     * @param id the id
122     * @return true if the primitive with id <code>id</code> was created in this
123     * changeset.
124     */
125    public boolean isCreated(PrimitiveId id) {
126        ChangesetDataSetEntry e = getFirstEntry(id);
127        return e != null && e.getModificationType() == ChangesetModificationType.CREATED;
128    }
129
130    /**
131     * Replies true if the primitive with id <code>id</code> was updated in this
132     * changeset. Replies false, if id is null or not in the dataset.
133     *
134     * @param id the id
135     * @return true if the primitive with id <code>id</code> was updated in this
136     * changeset.
137     */
138    public boolean isUpdated(PrimitiveId id) {
139        ChangesetDataSetEntry e = getLastEntry(id);
140        return e != null && e.getModificationType() == ChangesetModificationType.UPDATED;
141    }
142
143    /**
144     * Replies true if the primitive with id <code>id</code> was deleted in this
145     * changeset. Replies false, if id is null or not in the dataset.
146     *
147     * @param id the id
148     * @return true if the primitive with id <code>id</code> was deleted in this
149     * changeset.
150     */
151    public boolean isDeleted(PrimitiveId id) {
152        ChangesetDataSetEntry e = getLastEntry(id);
153        return e != null && e.getModificationType() == ChangesetModificationType.DELETED;
154    }
155
156    /**
157     * Replies the number of primitives in the dataset.
158     *
159     * @return the number of primitives in the dataset.
160     */
161    public int size() {
162        return entryMap.size();
163    }
164
165    /**
166     * Replies the {@link HistoryOsmPrimitive} with id <code>id</code> from this dataset.
167     * null, if there is no such primitive in the data set. If the primitive was modified
168     * multiple times, the last version is returned.
169     *
170     * @param id the id
171     * @return the {@link HistoryOsmPrimitive} with id <code>id</code> from this dataset
172     */
173    public HistoryOsmPrimitive getPrimitive(PrimitiveId id) {
174        ChangesetDataSetEntry e = getLastEntry(id);
175        return e != null ? e.getPrimitive() : null;
176    }
177
178    /**
179     * @return an unmodifiable set of all primitives in this dataset.
180     * @since 14946
181     */
182    public Set<PrimitiveId> getIds() {
183        return Collections.unmodifiableSet(entryMap.keySet());
184    }
185
186    /**
187     * Replies the first {@link ChangesetDataSetEntry} with id <code>id</code> from this dataset.
188     * null, if there is no such primitive in the data set.
189     * @param id the id
190     * @return the first {@link ChangesetDataSetEntry} with id <code>id</code> from this dataset or null.
191     * @since 14946
192     */
193    public ChangesetDataSetEntry getFirstEntry(PrimitiveId id) {
194        return getEntry(id, 0);
195    }
196
197    /**
198     * Replies the last {@link ChangesetDataSetEntry} with id <code>id</code> from this dataset.
199     * null, if there is no such primitive in the data set.
200     * @param id the id
201     * @return the last {@link ChangesetDataSetEntry} with id <code>id</code> from this dataset or null.
202     * @since 14946
203     */
204    public ChangesetDataSetEntry getLastEntry(PrimitiveId id) {
205        return getEntry(id, 1);
206    }
207
208    private ChangesetDataSetEntry getEntry(PrimitiveId id, int n) {
209        if (id == null)
210            return null;
211        Object val = entryMap.get(id);
212        if (val == null)
213            return null;
214        if (val instanceof ChangesetDataSetEntry[]) {
215            ChangesetDataSetEntry[] entries = (ChangesetDataSetEntry[]) val;
216            return entries[n];
217        } else {
218            return (ChangesetDataSetEntry) val;
219        }
220    }
221
222    /**
223     * Returns an iterator over dataset entries. The elements are returned in no particular order.
224     * @return an iterator over dataset entries. If a primitive was changed multiple times, only the last entry is returned.
225     */
226    public Iterator<ChangesetDataSetEntry> iterator() {
227        return new DefaultIterator(entryMap);
228    }
229
230    /**
231     * Class to keep one entry of a changeset: the combination of modification type and primitive.
232     */
233    public static class DefaultChangesetDataSetEntry implements ChangesetDataSetEntry {
234        private final ChangesetModificationType modificationType;
235        private final HistoryOsmPrimitive primitive;
236
237        /**
238         * Construct new entry.
239         * @param modificationType the modification type
240         * @param primitive the primitive
241         */
242        public DefaultChangesetDataSetEntry(ChangesetModificationType modificationType, HistoryOsmPrimitive primitive) {
243            this.modificationType = modificationType;
244            this.primitive = primitive;
245        }
246
247        @Override
248        public ChangesetModificationType getModificationType() {
249            return modificationType;
250        }
251
252        @Override
253        public HistoryOsmPrimitive getPrimitive() {
254            return primitive;
255        }
256
257        @Override
258        public String toString() {
259            return modificationType.toString() + " " + primitive.toString();
260        }
261    }
262
263    private static class DefaultIterator implements Iterator<ChangesetDataSetEntry> {
264        private final Iterator<Entry<PrimitiveId, Object>> typeIterator;
265
266        DefaultIterator(Map<PrimitiveId, Object> entryMap) {
267            typeIterator = entryMap.entrySet().iterator();
268        }
269
270        @Override
271        public boolean hasNext() {
272            return typeIterator.hasNext();
273        }
274
275        @Override
276        public ChangesetDataSetEntry next() {
277            Entry<PrimitiveId, Object> next = typeIterator.next();
278            // get last entry
279            Object val = next.getValue();
280            ChangesetDataSetEntry last;
281            if (val instanceof ChangesetDataSetEntry[]) {
282                ChangesetDataSetEntry[] entries = (ChangesetDataSetEntry[]) val;
283                last = entries[1];
284            } else {
285                last = (ChangesetDataSetEntry) val;
286            }
287            return new DefaultChangesetDataSetEntry(last.getModificationType(), last.getPrimitive());
288        }
289
290        @Override
291        public void remove() {
292            throw new UnsupportedOperationException();
293        }
294    }
295}