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