// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.osm; import java.io.Serializable; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; /** * This class provides a read/write map that uses the same format as {@link AbstractPrimitive#keys}. * It offers good performance for few keys. * It uses copy on write, so there cannot be a {@link ConcurrentModificationException} while iterating through it. * * @author Michael Zangl */ public class TagMap extends AbstractMap<String, String> implements Serializable { static final long serialVersionUID = 1; /** * We use this array every time we want to represent an empty map. * This saves us the burden of checking for null every time but saves some object allocations. */ private static final String[] EMPTY_TAGS = new String[0]; /** * An iterator that iterates over the tags in this map. The iterator always represents the state of the map when it was created. * Further changes to the map won't change the tags that we iterate over but they also won't raise any exceptions. * @author Michael Zangl */ private static class TagEntryInterator implements Iterator<Entry<String, String>> { /** * The current state of the tags we iterate over. */ private final String[] tags; /** * Current tag index. Always a multiple of 2. */ private int currentIndex; /** * Create a new {@link TagEntryInterator} * @param tags The tags array. It is never changed but should also not be changed by you. */ TagEntryInterator(String... tags) { super(); this.tags = tags; } @Override public boolean hasNext() { return currentIndex < tags.length; } @Override public Entry<String, String> next() { if (!hasNext()) { throw new NoSuchElementException(); } Tag tag = new Tag(tags[currentIndex], tags[currentIndex + 1]); currentIndex += 2; return tag; } @Override public void remove() { throw new UnsupportedOperationException(); } } /** * This is the entry set of this map. It represents the state when it was created. * @author Michael Zangl */ private static class TagEntrySet extends AbstractSet<Entry<String, String>> { private final String[] tags; /** * Create a new {@link TagEntrySet} * @param tags The tags array. It is never changed but should also not be changed by you. */ TagEntrySet(String... tags) { super(); this.tags = tags; } @Override public Iterator<Entry<String, String>> iterator() { return new TagEntryInterator(tags); } @Override public int size() { return tags.length / 2; } } /** * The tags field. This field is guarded using RCU. */ private volatile String[] tags; /** * Creates a new, empty tag map. */ public TagMap() { this((String[]) null); } /** * Create a new tag map and load it from the other map. * @param tags The map to load from. * @since 10604 */ public TagMap(Map<String, String> tags) { putAll(tags); } /** * Copy constructor. * @param tagMap The map to copy from. * @since 10604 */ public TagMap(TagMap tagMap) { this(tagMap.tags); } /** * Creates a new read only tag map using a key/value/key/value/... array. * <p> * The array that is passed as parameter may not be modified after passing it to this map. * @param tags The tags array. It is not modified by this map. */ public TagMap(String... tags) { if (tags == null || tags.length == 0) { this.tags = EMPTY_TAGS; } else { if (tags.length % 2 != 0) { throw new IllegalArgumentException("tags array length needs to be multiple of two."); } this.tags = tags; } } /** * Creates a new map using the given list of tags. For dupplicate keys the last value found is used. * @param tags The tags * @since 10736 */ public TagMap(Collection<Tag> tags) { for (Tag tag : tags) { put(tag.getKey(), tag.getValue()); } } @Override public Set<Entry<String, String>> entrySet() { return new TagEntrySet(tags); } @Override public boolean containsKey(Object key) { return indexOfKey(tags, key) >= 0; } @Override public String get(Object key) { String[] tags = this.tags; int index = indexOfKey(tags, key); return index < 0 ? null : tags[index + 1]; } @Override public boolean containsValue(Object value) { String[] tags = this.tags; for (int i = 1; i < tags.length; i += 2) { if (value.equals(tags[i])) { return true; } } return false; } @Override public synchronized String put(String key, String value) { Objects.requireNonNull(key); Objects.requireNonNull(value); int index = indexOfKey(tags, key); int newTagArrayLength = tags.length; if (index < 0) { index = newTagArrayLength; newTagArrayLength += 2; } String[] newTags = Arrays.copyOf(tags, newTagArrayLength); String old = newTags[index + 1]; newTags[index] = key; newTags[index + 1] = value; tags = newTags; return old; } @Override public synchronized String remove(Object key) { int index = indexOfKey(tags, key); if (index < 0) { return null; } String old = tags[index + 1]; int newLength = tags.length - 2; if (newLength == 0) { tags = EMPTY_TAGS; } else { String[] newTags = new String[newLength]; System.arraycopy(tags, 0, newTags, 0, index); System.arraycopy(tags, index + 2, newTags, index, newLength - index); tags = newTags; } return old; } @Override public synchronized void clear() { tags = EMPTY_TAGS; } @Override public int size() { return tags.length / 2; } /** * Gets a list of all tags contained in this map. * @return The list of tags in the order they were added. * @since 10604 */ public List<Tag> getTags() { List<Tag> tagList = new ArrayList<>(); for (int i = 0; i < tags.length; i += 2) { tagList.add(new Tag(tags[i], tags[i+1])); } return tagList; } /** * Finds a key in an array that is structured like the {@link #tags} array and returns the position. * <p> * We allow the parameter to be passed to allow for better synchronization. * * @param tags The tags array to search through. * @param key The key to search. * @return The index of the key (a multiple of two) or -1 if it was not found. */ private static int indexOfKey(String[] tags, Object key) { for (int i = 0; i < tags.length; i += 2) { if (tags[i].equals(key)) { return i; } } return -1; } @Override public String toString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("TagMap["); boolean first = true; for (Map.Entry<String, String> e : entrySet()) { if (!first) { stringBuilder.append(','); } stringBuilder.append(e.getKey()); stringBuilder.append('='); stringBuilder.append(e.getValue()); first = false; } stringBuilder.append(']'); return stringBuilder.toString(); } /** * Gets the backing tags array. Do not modify this array. * @return The tags array. */ String[] getTagsArray() { return tags; } }