001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.HashMap;
010import java.util.HashSet;
011import java.util.Iterator;
012import java.util.LinkedHashMap;
013import java.util.LinkedHashSet;
014import java.util.List;
015import java.util.Map;
016import java.util.Map.Entry;
017import java.util.Set;
018import java.util.regex.Pattern;
019
020import org.openstreetmap.josm.tools.Utils;
021
022/**
023 * TagCollection is a collection of tags which can be used to manipulate
024 * tags managed by {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s.
025 *
026 * A TagCollection can be created:
027 * <ul>
028 *  <li>from the tags managed by a specific {@link org.openstreetmap.josm.data.osm.OsmPrimitive} with {@link #from(org.openstreetmap.josm.data.osm.Tagged)}</li>
029 *  <li>from the union of all tags managed by a collection of {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s with {@link #unionOfAllPrimitives(java.util.Collection)}</li>
030 *  <li>from the union of all tags managed by a {@link org.openstreetmap.josm.data.osm.DataSet} with {@link #unionOfAllPrimitives(org.openstreetmap.josm.data.osm.DataSet)}</li>
031 *  <li>from the intersection of all tags managed by a collection of primitives with {@link #commonToAllPrimitives(java.util.Collection)}</li>
032 * </ul>
033 *
034 * It  provides methods to query the collection, like {@link #size()}, {@link #hasTagsFor(String)}, etc.
035 *
036 * Basic set operations allow to create the union, the intersection and  the difference
037 * of tag collections, see {@link #union(org.openstreetmap.josm.data.osm.TagCollection)}, {@link #intersect(org.openstreetmap.josm.data.osm.TagCollection)},
038 * and {@link #minus(org.openstreetmap.josm.data.osm.TagCollection)}.
039 *
040 *
041 */
042public class TagCollection implements Iterable<Tag> {
043
044    /**
045     * Creates a tag collection from the tags managed by a specific
046     * {@link org.openstreetmap.josm.data.osm.OsmPrimitive}. If <code>primitive</code> is null, replies
047     * an empty tag collection.
048     *
049     * @param primitive  the primitive
050     * @return a tag collection with the tags managed by a specific
051     * {@link org.openstreetmap.josm.data.osm.OsmPrimitive}
052     */
053    public static TagCollection from(Tagged primitive) {
054        TagCollection tags = new TagCollection();
055        if (primitive != null) {
056            for (String key: primitive.keySet()) {
057                tags.add(new Tag(key, primitive.get(key)));
058            }
059        }
060        return tags;
061    }
062
063    /**
064     * Creates a tag collection from a map of key/value-pairs. Replies
065     * an empty tag collection if {@code tags} is null.
066     *
067     * @param tags  the key/value-pairs
068     * @return the tag collection
069     */
070    public static TagCollection from(Map<String,String> tags) {
071        TagCollection ret = new TagCollection();
072        if (tags == null) return ret;
073        for (Entry<String,String> entry: tags.entrySet()) {
074            String key = entry.getKey() == null? "" : entry.getKey();
075            String value = entry.getValue() == null ? "" : entry.getValue();
076            ret.add(new Tag(key,value));
077        }
078        return ret;
079    }
080
081    /**
082     * Creates a tag collection from the union of the tags managed by
083     * a collection of primitives. Replies an empty tag collection,
084     * if <code>primitives</code> is null.
085     *
086     * @param primitives the primitives
087     * @return  a tag collection with the union of the tags managed by
088     * a collection of primitives
089     */
090    public static TagCollection unionOfAllPrimitives(Collection<? extends Tagged> primitives) {
091        TagCollection tags = new TagCollection();
092        if (primitives == null) return tags;
093        for (Tagged primitive: primitives) {
094            if (primitive == null) {
095                continue;
096            }
097            tags.add(TagCollection.from(primitive));
098        }
099        return tags;
100    }
101
102    /**
103     * Replies a tag collection with the tags which are common to all primitives in in
104     * <code>primitives</code>. Replies an empty tag collection of <code>primitives</code>
105     * is null.
106     *
107     * @param primitives the primitives
108     * @return  a tag collection with the tags which are common to all primitives
109     */
110    public static TagCollection commonToAllPrimitives(Collection<? extends Tagged> primitives) {
111        TagCollection tags = new TagCollection();
112        if (primitives == null || primitives.isEmpty()) return tags;
113        // initialize with the first
114        //
115        tags.add(TagCollection.from(primitives.iterator().next()));
116
117        // intersect with the others
118        //
119        for (Tagged primitive: primitives) {
120            if (primitive == null) {
121                continue;
122            }
123            tags.add(tags.intersect(TagCollection.from(primitive)));
124        }
125        return tags;
126    }
127
128    /**
129     * Replies a tag collection with the union of the tags which are common to all primitives in
130     * the dataset <code>ds</code>. Returns an empty tag collection of <code>ds</code> is null.
131     *
132     * @param ds the dataset
133     * @return a tag collection with the union of the tags which are common to all primitives in
134     * the dataset <code>ds</code>
135     */
136    public static TagCollection unionOfAllPrimitives(DataSet ds) {
137        TagCollection tags = new TagCollection();
138        if (ds == null) return tags;
139        tags.add(TagCollection.unionOfAllPrimitives(ds.allPrimitives()));
140        return tags;
141    }
142
143    private final Set<Tag> tags = new HashSet<>();
144
145    /**
146     * Creates an empty tag collection
147     */
148    public TagCollection() {
149    }
150
151    /**
152     * Creates a clone of the tag collection <code>other</code>. Creats an empty
153     * tag collection if <code>other</code> is null.
154     *
155     * @param other the other collection
156     */
157    public TagCollection(TagCollection other) {
158        if (other != null) {
159            tags.addAll(other.tags);
160        }
161    }
162
163    /**
164     * Creates a tag collection from <code>tags</code>.
165     * @param tags the collection of tags
166     * @since 5724
167     */
168    public TagCollection(Collection<Tag> tags) {
169        add(tags);
170    }
171
172    /**
173     * Replies the number of tags in this tag collection
174     *
175     * @return the number of tags in this tag collection
176     */
177    public int size() {
178        return tags.size();
179    }
180
181    /**
182     * Replies true if this tag collection is empty
183     *
184     * @return true if this tag collection is empty; false, otherwise
185     */
186    public boolean isEmpty() {
187        return size() == 0;
188    }
189
190    /**
191     * Adds a tag to the tag collection. If <code>tag</code> is null, nothing is added.
192     *
193     * @param tag the tag to add
194     */
195    public final void add(Tag tag){
196        if (tag == null) return;
197        if (tags.contains(tag)) return;
198        tags.add(tag);
199    }
200
201    /**
202     * Adds a collection of tags to the tag collection. If <code>tags</code> is null, nothing
203     * is added. null values in the collection are ignored.
204     *
205     * @param tags the collection of tags
206     */
207    public final void add(Collection<Tag> tags) {
208        if (tags == null) return;
209        for (Tag tag: tags){
210            add(tag);
211        }
212    }
213
214    /**
215     * Adds the tags of another tag collection to this collection. Adds nothing, if
216     * <code>tags</code> is null.
217     *
218     * @param tags the other tag collection
219     */
220    public final void add(TagCollection tags) {
221        if (tags == null) return;
222        this.tags.addAll(tags.tags);
223    }
224
225    /**
226     * Removes a specific tag from the tag collection. Does nothing if <code>tag</code> is
227     * null.
228     *
229     * @param tag the tag to be removed
230     */
231    public void remove(Tag tag) {
232        if (tag == null) return;
233        tags.remove(tag);
234    }
235
236    /**
237     * Removes a collection of tags from the tag collection. Does nothing if <code>tags</code> is
238     * null.
239     *
240     * @param tags the tags to be removed
241     */
242    public void remove(Collection<Tag> tags) {
243        if (tags == null) return;
244        this.tags.removeAll(tags);
245    }
246
247    /**
248     * Removes all tags in the tag collection <code>tags</code> from the current tag collection.
249     * Does nothing if <code>tags</code> is null.
250     *
251     * @param tags the tag collection to be removed.
252     */
253    public void remove(TagCollection tags) {
254        if (tags == null) return;
255        this.tags.removeAll(tags.tags);
256    }
257
258    /**
259     * Removes all tags whose keys are equal to  <code>key</code>. Does nothing if <code>key</code>
260     * is null.
261     *
262     * @param key the key to be removed
263     */
264    public void removeByKey(String key) {
265        if (key  == null) return;
266        Iterator<Tag> it = tags.iterator();
267        while(it.hasNext()) {
268            if (it.next().matchesKey(key)) {
269                it.remove();
270            }
271        }
272    }
273
274    /**
275     * Removes all tags whose key is in the collection <code>keys</code>. Does nothing if
276     * <code>keys</code> is null.
277     *
278     * @param keys the collection of keys to be removed
279     */
280    public void removeByKey(Collection<String> keys) {
281        if (keys == null) return;
282        for (String key: keys) {
283            removeByKey(key);
284        }
285    }
286
287    /**
288     * Replies true if the this tag collection contains <code>tag</code>.
289     *
290     * @param tag the tag to look up
291     * @return true if the this tag collection contains <code>tag</code>; false, otherwise
292     */
293    public boolean contains(Tag tag) {
294        return tags.contains(tag);
295    }
296
297    /**
298     * Replies true if this tag collection contains at least one tag with key <code>key</code>.
299     *
300     * @param key the key to look up
301     * @return true if this tag collection contains at least one tag with key <code>key</code>; false, otherwise
302     */
303    public boolean containsKey(String key) {
304        if (key == null) return false;
305        for (Tag tag: tags) {
306            if (tag.matchesKey(key)) return true;
307        }
308        return false;
309    }
310
311    /**
312     * Replies true if this tag collection contains all tags in <code>tags</code>. Replies
313     * false, if tags is null.
314     *
315     * @param tags the tags to look up
316     * @return true if this tag collection contains all tags in <code>tags</code>. Replies
317     * false, if tags is null.
318     */
319    public boolean containsAll(Collection<Tag> tags) {
320        if (tags == null) return false;
321        return this.tags.containsAll(tags);
322    }
323
324    /**
325     * Replies true if this tag collection at least one tag for every key in <code>keys</code>.
326     * Replies false, if <code>keys</code> is null. null values in <code>keys</code> are ignored.
327     *
328     * @param keys the keys to lookup
329     * @return true if this tag collection at least one tag for every key in <code>keys</code>.
330     */
331    public boolean containsAllKeys(Collection<String> keys) {
332        if (keys == null) return false;
333        for (String key: keys) {
334            if (key == null) {
335                continue;
336            }
337            if (! containsKey(key)) return false;
338        }
339        return true;
340    }
341
342    /**
343     * Replies the number of tags with key <code>key</code>
344     *
345     * @param key the key to look up
346     * @return the number of tags with key <code>key</code>. 0, if key is null.
347     */
348    public int getNumTagsFor(String key) {
349        if (key == null) return 0;
350        int count = 0;
351        for (Tag tag: tags) {
352            if (tag.matchesKey(key)) {
353                count++;
354            }
355        }
356        return count;
357    }
358
359    /**
360     * Replies true if there is at least one tag for the given key.
361     *
362     * @param key the key to look up
363     * @return true if there is at least one tag for the given key. false, if key is null.
364     */
365    public boolean hasTagsFor(String key) {
366        return getNumTagsFor(key) > 0;
367    }
368
369    /**
370     * Replies true it there is at least one tag with a non empty value for key.
371     * Replies false if key is null.
372     *
373     * @param key the key
374     * @return true it there is at least one tag with a non empty value for key.
375     */
376    public boolean hasValuesFor(String key) {
377        if (key == null) return false;
378        Set<String> values = getTagsFor(key).getValues();
379        values.remove("");
380        return !values.isEmpty();
381    }
382
383    /**
384     * Replies true if there is exactly one tag for <code>key</code> and
385     * if the value of this tag is not empty. Replies false if key is
386     * null.
387     *
388     * @param key the key
389     * @return true if there is exactly one tag for <code>key</code> and
390     * if the value of this tag is not empty
391     */
392    public boolean hasUniqueNonEmptyValue(String key) {
393        if (key == null) return false;
394        Set<String> values = getTagsFor(key).getValues();
395        return values.size() == 1 && ! values.contains("");
396    }
397
398    /**
399     * Replies true if there is a tag with an empty value for <code>key</code>.
400     * Replies false, if key is null.
401     *
402     * @param key the key
403     * @return true if there is a tag with an empty value for <code>key</code>
404     */
405    public boolean hasEmptyValue(String key) {
406        if (key == null) return false;
407        Set<String> values = getTagsFor(key).getValues();
408        return values.contains("");
409    }
410
411    /**
412     * Replies true if there is exactly one tag for <code>key</code> and if
413     * the value for this tag is empty. Replies false if key is null.
414     *
415     * @param key the key
416     * @return  true if there is exactly one tag for <code>key</code> and if
417     * the value for this tag is empty
418     */
419    public boolean hasUniqueEmptyValue(String key) {
420        if (key == null) return false;
421        Set<String> values = getTagsFor(key).getValues();
422        return values.size() == 1 && values.contains("");
423    }
424
425    /**
426     * Replies a tag collection with the tags for a given key. Replies an empty collection
427     * if key is null.
428     *
429     * @param key the key to look up
430     * @return a tag collection with the tags for a given key. Replies an empty collection
431     * if key is null.
432     */
433    public TagCollection getTagsFor(String key) {
434        TagCollection ret = new TagCollection();
435        if (key == null)
436            return ret;
437        for (Tag tag: tags) {
438            if (tag.matchesKey(key)) {
439                ret.add(tag);
440            }
441        }
442        return ret;
443    }
444
445    /**
446     * Replies a tag collection with all tags whose key is equal to one of the keys in
447     * <code>keys</code>. Replies an empty collection if keys is null.
448     *
449     * @param keys the keys to look up
450     * @return a tag collection with all tags whose key is equal to one of the keys in
451     * <code>keys</code>
452     */
453    public TagCollection getTagsFor(Collection<String> keys) {
454        TagCollection ret = new TagCollection();
455        if (keys == null)
456            return ret;
457        for(String key : keys) {
458            if (key != null) {
459                ret.add(getTagsFor(key));
460            }
461        }
462        return ret;
463    }
464
465    /**
466     * Replies the tags of this tag collection as set
467     *
468     * @return the tags of this tag collection as set
469     */
470    public Set<Tag> asSet() {
471        return new HashSet<>(tags);
472    }
473
474    /**
475     * Replies the tags of this tag collection as list.
476     * Note that the order of the list is not preserved between method invocations.
477     *
478     * @return the tags of this tag collection as list.
479     */
480    public List<Tag> asList() {
481        return new ArrayList<>(tags);
482    }
483
484    /**
485     * Replies an iterator to iterate over the tags in this collection
486     *
487     * @return the iterator
488     */
489    @Override
490    public Iterator<Tag> iterator() {
491        return tags.iterator();
492    }
493
494    /**
495     * Replies the set of keys of this tag collection.
496     *
497     * @return the set of keys of this tag collection
498     */
499    public Set<String> getKeys() {
500        HashSet<String> ret = new HashSet<>();
501        for (Tag tag: tags) {
502            ret.add(tag.getKey());
503        }
504        return ret;
505    }
506
507    /**
508     * Replies the set of keys which have at least 2 matching tags.
509     *
510     * @return the set of keys which have at least 2 matching tags.
511     */
512    public Set<String> getKeysWithMultipleValues() {
513        HashMap<String, Integer> counters = new HashMap<>();
514        for (Tag tag: tags) {
515            Integer v = counters.get(tag.getKey());
516            counters.put(tag.getKey(),(v==null) ? 1 : v+1);
517        }
518        Set<String> ret = new HashSet<>();
519        for (Entry<String, Integer> e : counters.entrySet()) {
520            if (e.getValue() > 1) {
521                ret.add(e.getKey());
522            }
523        }
524        return ret;
525    }
526
527    /**
528     * Sets a unique tag for the key of this tag. All other tags with the same key are
529     * removed from the collection. Does nothing if tag is null.
530     *
531     * @param tag the tag to set
532     */
533    public void setUniqueForKey(Tag tag) {
534        if (tag == null) return;
535        removeByKey(tag.getKey());
536        add(tag);
537    }
538
539    /**
540     * Sets a unique tag for the key of this tag. All other tags with the same key are
541     * removed from the collection. Assume the empty string for key and value if either
542     * key or value is null.
543     *
544     * @param key the key
545     * @param value the value
546     */
547    public void setUniqueForKey(String key, String value) {
548        Tag tag = new Tag(key, value);
549        setUniqueForKey(tag);
550    }
551
552    /**
553     * Replies the set of values in this tag collection
554     *
555     * @return the set of values
556     */
557    public Set<String> getValues() {
558        HashSet<String> ret = new HashSet<>();
559        for (Tag tag: tags) {
560            ret.add(tag.getValue());
561        }
562        return ret;
563    }
564
565    /**
566     * Replies the set of values for a given key. Replies an empty collection if there
567     * are no values for the given key.
568     *
569     * @param key the key to look up
570     * @return the set of values for a given key. Replies an empty collection if there
571     * are no values for the given key
572     */
573    public Set<String> getValues(String key) {
574        HashSet<String> ret = new HashSet<>();
575        if (key == null) return ret;
576        for (Tag tag: tags) {
577            if (tag.matchesKey(key)) {
578                ret.add(tag.getValue());
579            }
580        }
581        return ret;
582    }
583
584    /**
585     * Replies true if for every key there is one tag only, i.e. exactly one value.
586     *
587     * @return {@code true} if for every key there is one tag only
588     */
589    public boolean isApplicableToPrimitive() {
590        return size() == getKeys().size();
591    }
592
593    /**
594     * Applies this tag collection to an {@link org.openstreetmap.josm.data.osm.OsmPrimitive}. Does nothing if
595     * primitive is null
596     *
597     * @param primitive  the primitive
598     * @throws IllegalStateException thrown if this tag collection can't be applied
599     * because there are keys with multiple values
600     */
601    public void applyTo(Tagged primitive) throws IllegalStateException {
602        if (primitive == null) return;
603        if (! isApplicableToPrimitive())
604            throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values."));
605        for (Tag tag: tags) {
606            if (tag.getValue() == null || tag.getValue().isEmpty()) {
607                primitive.remove(tag.getKey());
608            } else {
609                primitive.put(tag.getKey(), tag.getValue());
610            }
611        }
612    }
613
614    /**
615     * Applies this tag collection to a collection of {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s. Does nothing if
616     * primitives is null
617     *
618     * @param primitives  the collection of primitives
619     * @throws IllegalStateException thrown if this tag collection can't be applied
620     * because there are keys with multiple values
621     */
622    public void applyTo(Collection<? extends Tagged> primitives) throws IllegalStateException{
623        if (primitives == null) return;
624        if (! isApplicableToPrimitive())
625            throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values."));
626        for (Tagged primitive: primitives) {
627            applyTo(primitive);
628        }
629    }
630
631    /**
632     * Replaces the tags of an {@link org.openstreetmap.josm.data.osm.OsmPrimitive} by the tags in this collection . Does nothing if
633     * primitive is null
634     *
635     * @param primitive  the primitive
636     * @throws IllegalStateException thrown if this tag collection can't be applied
637     * because there are keys with multiple values
638     */
639    public void replaceTagsOf(Tagged primitive) throws IllegalStateException {
640        if (primitive == null) return;
641        if (! isApplicableToPrimitive())
642            throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values."));
643        primitive.removeAll();
644        for (Tag tag: tags) {
645            primitive.put(tag.getKey(), tag.getValue());
646        }
647    }
648
649    /**
650     * Replaces the tags of a collection of{@link org.openstreetmap.josm.data.osm.OsmPrimitive}s by the tags in this collection.
651     * Does nothing if primitives is null
652     *
653     * @param primitives the collection of primitives
654     * @throws IllegalStateException thrown if this tag collection can't be applied
655     * because there are keys with multiple values
656     */
657    public void replaceTagsOf(Collection<? extends Tagged> primitives) throws IllegalStateException {
658        if (primitives == null) return;
659        if (! isApplicableToPrimitive())
660            throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values."));
661        for (Tagged primitive: primitives) {
662            replaceTagsOf(primitive);
663        }
664    }
665
666    /**
667     * Builds the intersection of this tag collection and another tag collection
668     *
669     * @param other the other tag collection. If null, replies an empty tag collection.
670     * @return the intersection of this tag collection and another tag collection
671     */
672    public TagCollection intersect(TagCollection other) {
673        TagCollection ret = new TagCollection();
674        if (other != null) {
675            for (Tag tag: tags) {
676                if (other.contains(tag)) {
677                    ret.add(tag);
678                }
679            }
680        }
681        return ret;
682    }
683
684    /**
685     * Replies the difference of this tag collection and another tag collection
686     *
687     * @param other the other tag collection. May be null.
688     * @return the difference of this tag collection and another tag collection
689     */
690    public TagCollection minus(TagCollection other) {
691        TagCollection ret = new TagCollection(this);
692        if (other != null) {
693            ret.remove(other);
694        }
695        return ret;
696    }
697
698    /**
699     * Replies the union of this tag collection and another tag collection
700     *
701     * @param other the other tag collection. May be null.
702     * @return the union of this tag collection and another tag collection
703     */
704    public TagCollection union(TagCollection other) {
705        TagCollection ret = new TagCollection(this);
706        if (other != null) {
707            ret.add(other);
708        }
709        return ret;
710    }
711
712    public TagCollection emptyTagsForKeysMissingIn(TagCollection other) {
713        TagCollection ret = new TagCollection();
714        for(String key: this.minus(other).getKeys()) {
715            ret.add(new Tag(key));
716        }
717        return ret;
718    }
719
720    private static final Pattern SPLIT_VALUES_PATTERN = Pattern.compile(";\\s*");
721
722    /**
723     * Replies the concatenation of all tag values (concatenated by a semicolon)
724     *
725     * @return the concatenation of all tag values
726     */
727    public String getJoinedValues(String key) {
728
729        // See #7201 combining ways screws up the order of ref tags
730        Set<String> originalValues = getValues(key);
731        if (originalValues.size() == 1) {
732            return originalValues.iterator().next();
733        }
734
735        Set<String> values = new LinkedHashSet<>();
736        Map<String, Collection<String>> originalSplitValues = new LinkedHashMap<>();
737        for (String v : originalValues) {
738            List<String> vs = Arrays.asList(SPLIT_VALUES_PATTERN.split(v));
739            originalSplitValues.put(v, vs);
740            values.addAll(vs);
741        }
742        values.remove("");
743        // try to retain an already existing key if it contains all needed values (remove this if it causes performance problems)
744        for (Entry<String, Collection<String>> i : originalSplitValues.entrySet()) {
745            if (i.getValue().containsAll(values)) {
746                return i.getKey();
747            }
748        }
749        return Utils.join(";", values);
750    }
751
752    @Override
753    public String toString() {
754        return tags.toString();
755    }
756}