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 14302
219     */
220    public int getTagOccurrence(Tag tag) {
221        return tags.getOrDefault(tag, 0);
222    }
223
224    /**
225     * Adds a collection of tags to the tag collection. If <code>tags</code> is null, nothing
226     * is added. null values in the collection are ignored.
227     *
228     * @param tags the collection of tags
229     */
230    public final void add(Collection<Tag> tags) {
231        if (tags == null) return;
232        for (Tag tag: tags) {
233            add(tag);
234        }
235    }
236
237    /**
238     * Adds the tags of another tag collection to this collection. Adds nothing, if
239     * <code>tags</code> is null.
240     *
241     * @param tags the other tag collection
242     */
243    public final void add(TagCollection tags) {
244        if (tags != null) {
245            for (Entry<Tag, Integer> entry : tags.tags.entrySet()) {
246                this.tags.merge(entry.getKey(), entry.getValue(), (i, j) -> i + j);
247            }
248        }
249    }
250
251    /**
252     * Removes a specific tag from the tag collection. Does nothing if <code>tag</code> is
253     * null.
254     *
255     * @param tag the tag to be removed
256     */
257    public void remove(Tag tag) {
258        if (tag == null) return;
259        tags.remove(tag);
260    }
261
262    /**
263     * Removes a collection of tags from the tag collection. Does nothing if <code>tags</code> is
264     * null.
265     *
266     * @param tags the tags to be removed
267     */
268    public void remove(Collection<Tag> tags) {
269        if (tags != null) {
270            tags.stream().forEach(this::remove);
271        }
272    }
273
274    /**
275     * Removes all tags in the tag collection <code>tags</code> from the current tag collection.
276     * Does nothing if <code>tags</code> is null.
277     *
278     * @param tags the tag collection to be removed.
279     */
280    public void remove(TagCollection tags) {
281        if (tags != null) {
282            tags.tags.keySet().stream().forEach(this::remove);
283        }
284    }
285
286    /**
287     * Removes all tags whose keys are equal to  <code>key</code>. Does nothing if <code>key</code>
288     * is null.
289     *
290     * @param key the key to be removed
291     */
292    public void removeByKey(String key) {
293        if (key != null) {
294            tags.keySet().removeIf(tag -> tag.matchesKey(key));
295        }
296    }
297
298    /**
299     * Removes all tags whose key is in the collection <code>keys</code>. Does nothing if
300     * <code>keys</code> is null.
301     *
302     * @param keys the collection of keys to be removed
303     */
304    public void removeByKey(Collection<String> keys) {
305        if (keys == null) return;
306        for (String key: keys) {
307            removeByKey(key);
308        }
309    }
310
311    /**
312     * Replies true if the this tag collection contains <code>tag</code>.
313     *
314     * @param tag the tag to look up
315     * @return true if the this tag collection contains <code>tag</code>; false, otherwise
316     */
317    public boolean contains(Tag tag) {
318        return tags.containsKey(tag);
319    }
320
321    /**
322     * Replies true if this tag collection contains all tags in <code>tags</code>. Replies
323     * false, if tags is null.
324     *
325     * @param tags the tags to look up
326     * @return true if this tag collection contains all tags in <code>tags</code>. Replies
327     * false, if tags is null.
328     */
329    public boolean containsAll(Collection<Tag> tags) {
330        if (tags == null) {
331            return false;
332        } else {
333            return this.tags.keySet().containsAll(tags);
334        }
335    }
336
337    /**
338     * Replies true if this tag collection at least one tag for every key in <code>keys</code>.
339     * Replies false, if <code>keys</code> is null. null values in <code>keys</code> are ignored.
340     *
341     * @param keys the keys to lookup
342     * @return true if this tag collection at least one tag for every key in <code>keys</code>.
343     */
344    public boolean containsAllKeys(Collection<String> keys) {
345        if (keys == null) {
346            return false;
347        } else {
348            return keys.stream().filter(Objects::nonNull).allMatch(this::hasTagsFor);
349        }
350    }
351
352    /**
353     * Replies the number of tags with key <code>key</code>
354     *
355     * @param key the key to look up
356     * @return the number of tags with key <code>key</code>, including the empty "" value. 0, if key is null.
357     */
358    public int getNumTagsFor(String key) {
359        return (int) generateStreamForKey(key).count();
360    }
361
362    /**
363     * Replies true if there is at least one tag for the given key.
364     *
365     * @param key the key to look up
366     * @return true if there is at least one tag for the given key. false, if key is null.
367     */
368    public boolean hasTagsFor(String key) {
369        return getNumTagsFor(key) > 0;
370    }
371
372    /**
373     * Replies true it there is at least one tag with a non empty value for key.
374     * Replies false if key is null.
375     *
376     * @param key the key
377     * @return true it there is at least one tag with a non empty value for key.
378     */
379    public boolean hasValuesFor(String key) {
380        return generateStreamForKey(key).anyMatch(t -> !t.getValue().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        return generateStreamForKey(key).filter(t -> !t.getValue().isEmpty()).count() == 1;
394    }
395
396    /**
397     * Replies true if there is a tag with an empty value for <code>key</code>.
398     * Replies false, if key is null.
399     *
400     * @param key the key
401     * @return true if there is a tag with an empty value for <code>key</code>
402     */
403    public boolean hasEmptyValue(String key) {
404        return generateStreamForKey(key).anyMatch(t -> t.getValue().isEmpty());
405    }
406
407    /**
408     * Replies true if there is exactly one tag for <code>key</code> and if
409     * the value for this tag is empty. Replies false if key is null.
410     *
411     * @param key the key
412     * @return  true if there is exactly one tag for <code>key</code> and if
413     * the value for this tag is empty
414     */
415    public boolean hasUniqueEmptyValue(String key) {
416        Set<String> values = getValues(key);
417        return values.size() == 1 && values.contains("");
418    }
419
420    /**
421     * Replies a tag collection with the tags for a given key. Replies an empty collection
422     * if key is null.
423     *
424     * @param key the key to look up
425     * @return a tag collection with the tags for a given key. Replies an empty collection
426     * if key is null.
427     */
428    public TagCollection getTagsFor(String key) {
429        TagCollection ret = new TagCollection();
430        generateStreamForKey(key).forEach(ret::add);
431        return ret;
432    }
433
434    /**
435     * Replies a tag collection with all tags whose key is equal to one of the keys in
436     * <code>keys</code>. Replies an empty collection if keys is null.
437     *
438     * @param keys the keys to look up
439     * @return a tag collection with all tags whose key is equal to one of the keys in
440     * <code>keys</code>
441     */
442    public TagCollection getTagsFor(Collection<String> keys) {
443        TagCollection ret = new TagCollection();
444        if (keys == null)
445            return ret;
446        for (String key : keys) {
447            if (key != null) {
448                ret.add(getTagsFor(key));
449            }
450        }
451        return ret;
452    }
453
454    /**
455     * Replies the tags of this tag collection as set
456     *
457     * @return the tags of this tag collection as set
458     */
459    public Set<Tag> asSet() {
460        return new HashSet<>(tags.keySet());
461    }
462
463    /**
464     * Replies the tags of this tag collection as list.
465     * Note that the order of the list is not preserved between method invocations.
466     *
467     * @return the tags of this tag collection as list. There are no dupplicate values.
468     */
469    public List<Tag> asList() {
470        return new ArrayList<>(tags.keySet());
471    }
472
473    /**
474     * Replies an iterator to iterate over the tags in this collection
475     *
476     * @return the iterator
477     */
478    @Override
479    public Iterator<Tag> iterator() {
480        return tags.keySet().iterator();
481    }
482
483    /**
484     * Replies the set of keys of this tag collection.
485     *
486     * @return the set of keys of this tag collection
487     */
488    public Set<String> getKeys() {
489        return generateKeyStream().collect(Collectors.toCollection(HashSet::new));
490    }
491
492    /**
493     * Replies the set of keys which have at least 2 matching tags.
494     *
495     * @return the set of keys which have at least 2 matching tags.
496     */
497    public Set<String> getKeysWithMultipleValues() {
498        HashSet<String> singleKeys = new HashSet<>();
499        return generateKeyStream().filter(key -> !singleKeys.add(key)).collect(Collectors.toSet());
500    }
501
502    /**
503     * Sets a unique tag for the key of this tag. All other tags with the same key are
504     * removed from the collection. Does nothing if tag is null.
505     *
506     * @param tag the tag to set
507     */
508    public void setUniqueForKey(Tag tag) {
509        if (tag == null) return;
510        removeByKey(tag.getKey());
511        add(tag);
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. Assume the empty string for key and value if either
517     * key or value is null.
518     *
519     * @param key the key
520     * @param value the value
521     */
522    public void setUniqueForKey(String key, String value) {
523        Tag tag = new Tag(key, value);
524        setUniqueForKey(tag);
525    }
526
527    /**
528     * Replies the set of values in this tag collection
529     *
530     * @return the set of values
531     */
532    public Set<String> getValues() {
533        return tags.keySet().stream().map(Tag::getValue).collect(Collectors.toSet());
534    }
535
536    /**
537     * Replies the set of values for a given key. Replies an empty collection if there
538     * are no values for the given key.
539     *
540     * @param key the key to look up
541     * @return the set of values for a given key. Replies an empty collection if there
542     * are no values for the given key
543     */
544    public Set<String> getValues(String key) {
545        // null-safe
546        return generateStreamForKey(key).map(Tag::getValue).collect(Collectors.toSet());
547    }
548
549    /**
550     * Replies true if for every key there is one tag only, i.e. exactly one value.
551     *
552     * @return {@code true} if for every key there is one tag only
553     */
554    public boolean isApplicableToPrimitive() {
555        return getKeysWithMultipleValues().isEmpty();
556    }
557
558    /**
559     * Applies this tag collection to an {@link org.openstreetmap.josm.data.osm.OsmPrimitive}. Does nothing if
560     * primitive is null
561     *
562     * @param primitive  the primitive
563     * @throws IllegalStateException if this tag collection can't be applied
564     * because there are keys with multiple values
565     */
566    public void applyTo(Tagged primitive) {
567        if (primitive == null) return;
568        ensureApplicableToPrimitive();
569        for (Tag tag: tags.keySet()) {
570            if (tag.getValue() == null || tag.getValue().isEmpty()) {
571                primitive.remove(tag.getKey());
572            } else {
573                primitive.put(tag.getKey(), tag.getValue());
574            }
575        }
576    }
577
578    /**
579     * Applies this tag collection to a collection of {@link org.openstreetmap.josm.data.osm.OsmPrimitive}s. Does nothing if
580     * primitives is null
581     *
582     * @param primitives the collection of primitives
583     * @throws IllegalStateException if this tag collection can't be applied
584     * because there are keys with multiple values
585     */
586    public void applyTo(Collection<? extends Tagged> primitives) {
587        if (primitives == null) return;
588        ensureApplicableToPrimitive();
589        for (Tagged primitive: primitives) {
590            applyTo(primitive);
591        }
592    }
593
594    /**
595     * Replaces the tags of an {@link org.openstreetmap.josm.data.osm.OsmPrimitive} by the tags in this collection . Does nothing if
596     * primitive is null
597     *
598     * @param primitive  the primitive
599     * @throws IllegalStateException if this tag collection can't be applied
600     * because there are keys with multiple values
601     */
602    public void replaceTagsOf(Tagged primitive) {
603        if (primitive == null) return;
604        ensureApplicableToPrimitive();
605        primitive.removeAll();
606        for (Tag tag: tags.keySet()) {
607            primitive.put(tag.getKey(), tag.getValue());
608        }
609    }
610
611    /**
612     * Replaces the tags of a collection of{@link org.openstreetmap.josm.data.osm.OsmPrimitive}s by the tags in this collection.
613     * Does nothing if primitives is null
614     *
615     * @param primitives the collection of primitives
616     * @throws IllegalStateException if this tag collection can't be applied
617     * because there are keys with multiple values
618     */
619    public void replaceTagsOf(Collection<? extends Tagged> primitives) {
620        if (primitives == null) return;
621        ensureApplicableToPrimitive();
622        for (Tagged primitive: primitives) {
623            replaceTagsOf(primitive);
624        }
625    }
626
627    private void ensureApplicableToPrimitive() {
628        if (!isApplicableToPrimitive())
629            throw new IllegalStateException(tr("Tag collection cannot be applied to a primitive because there are keys with multiple values."));
630    }
631
632    /**
633     * Builds the intersection of this tag collection and another tag collection
634     *
635     * @param other the other tag collection. If null, replies an empty tag collection.
636     * @return the intersection of this tag collection and another tag collection. All counts are set to 1.
637     */
638    public TagCollection intersect(TagCollection other) {
639        TagCollection ret = new TagCollection();
640        if (other != null) {
641            tags.keySet().stream().filter(other::contains).forEach(ret::add);
642        }
643        return ret;
644    }
645
646    /**
647     * Replies the difference of this tag collection and another tag collection
648     *
649     * @param other the other tag collection. May be null.
650     * @return the difference of this tag collection and another tag collection
651     */
652    public TagCollection minus(TagCollection other) {
653        TagCollection ret = new TagCollection(this);
654        if (other != null) {
655            ret.remove(other);
656        }
657        return ret;
658    }
659
660    /**
661     * Replies the union of this tag collection and another tag collection
662     *
663     * @param other the other tag collection. May be null.
664     * @return the union of this tag collection and another tag collection. The tag count is summed.
665     */
666    public TagCollection union(TagCollection other) {
667        TagCollection ret = new TagCollection(this);
668        if (other != null) {
669            ret.add(other);
670        }
671        return ret;
672    }
673
674    public TagCollection emptyTagsForKeysMissingIn(TagCollection other) {
675        TagCollection ret = new TagCollection();
676        for (String key: this.minus(other).getKeys()) {
677            ret.add(new Tag(key));
678        }
679        return ret;
680    }
681
682    private static final Pattern SPLIT_VALUES_PATTERN = Pattern.compile(";\\s*");
683
684    /**
685     * Replies the concatenation of all tag values (concatenated by a semicolon)
686     * @param key the key to look up
687     *
688     * @return the concatenation of all tag values
689     */
690    public String getJoinedValues(String key) {
691
692        // See #7201 combining ways screws up the order of ref tags
693        Set<String> originalValues = getValues(key);
694        if (originalValues.size() == 1) {
695            return originalValues.iterator().next();
696        }
697
698        Set<String> values = new LinkedHashSet<>();
699        Map<String, Collection<String>> originalSplitValues = new LinkedHashMap<>();
700        for (String v : originalValues) {
701            List<String> vs = Arrays.asList(SPLIT_VALUES_PATTERN.split(v));
702            originalSplitValues.put(v, vs);
703            values.addAll(vs);
704        }
705        values.remove("");
706        // try to retain an already existing key if it contains all needed values (remove this if it causes performance problems)
707        for (Entry<String, Collection<String>> i : originalSplitValues.entrySet()) {
708            if (i.getValue().containsAll(values)) {
709                return i.getKey();
710            }
711        }
712        return Utils.join(";", values);
713    }
714
715    /**
716     * Replies the sum of all numeric tag values. Ignores dupplicates.
717     * @param key the key to look up
718     *
719     * @return the sum of all numeric tag values, as string.
720     * @since 7743
721     */
722    public String getSummedValues(String key) {
723        int result = 0;
724        for (String value : getValues(key)) {
725            try {
726                result += Integer.parseInt(value);
727            } catch (NumberFormatException e) {
728                Logging.trace(e);
729            }
730        }
731        return Integer.toString(result);
732    }
733
734    private Stream<String> generateKeyStream() {
735        return tags.keySet().stream().map(Tag::getKey);
736    }
737
738    /**
739     * Get a stram for the given key.
740     * @param key The key
741     * @return The stream. An empty stream if key is <code>null</code>
742     */
743    private Stream<Tag> generateStreamForKey(String key) {
744        return tags.keySet().stream().filter(e -> e.matchesKey(key));
745    }
746
747    @Override
748    public String toString() {
749        return tags.toString();
750    }
751}