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