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.text.MessageFormat;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.Date;
011import java.util.HashMap;
012import java.util.HashSet;
013import java.util.Map;
014import java.util.Map.Entry;
015import java.util.Objects;
016import java.util.Set;
017import java.util.concurrent.atomic.AtomicLong;
018
019import org.openstreetmap.josm.tools.LanguageInfo;
020import org.openstreetmap.josm.tools.Utils;
021
022/**
023* Abstract class to represent common features of the datatypes primitives.
024*
025* @since 4099
026*/
027public abstract class AbstractPrimitive implements IPrimitive {
028
029    /**
030     * This is a visitor that can be used to loop over the keys/values of this primitive.
031     *
032     * @author Michael Zangl
033     * @since 8742
034     */
035    public interface KeyValueVisitor {
036
037        /**
038         * This method gets called for every tag received.
039         *
040         * @param primitive This primitive
041         * @param key   The key
042         * @param value The value
043         */
044        void visitKeyValue(AbstractPrimitive primitive, String key, String value);
045    }
046
047    private static final AtomicLong idCounter = new AtomicLong(0);
048
049    static long generateUniqueId() {
050        return idCounter.decrementAndGet();
051    }
052
053    /**
054     * This flag shows, that the properties have been changed by the user
055     * and on upload the object will be send to the server.
056     */
057    protected static final int FLAG_MODIFIED = 1 << 0;
058
059    /**
060     * This flag is false, if the object is marked
061     * as deleted on the server.
062     */
063    protected static final int FLAG_VISIBLE  = 1 << 1;
064
065    /**
066     * An object that was deleted by the user.
067     * Deleted objects are usually hidden on the map and a request
068     * for deletion will be send to the server on upload.
069     * An object usually cannot be deleted if it has non-deleted
070     * objects still referring to it.
071     */
072    protected static final int FLAG_DELETED  = 1 << 2;
073
074    /**
075     * A primitive is incomplete if we know its id and type, but nothing more.
076     * Typically some members of a relation are incomplete until they are
077     * fetched from the server.
078     */
079    protected static final int FLAG_INCOMPLETE = 1 << 3;
080
081    /**
082     * Put several boolean flags to one short int field to save memory.
083     * Other bits of this field are used in subclasses.
084     */
085    protected volatile short flags = FLAG_VISIBLE;   // visible per default
086
087    /*-------------------
088     * OTHER PROPERTIES
089     *-------------------*/
090
091    /**
092     * Unique identifier in OSM. This is used to identify objects on the server.
093     * An id of 0 means an unknown id. The object has not been uploaded yet to
094     * know what id it will get.
095     */
096    protected long id;
097
098    /**
099     * User that last modified this primitive, as specified by the server.
100     * Never changed by JOSM.
101     */
102    protected User user;
103
104    /**
105     * Contains the version number as returned by the API. Needed to
106     * ensure update consistency
107     */
108    protected int version;
109
110    /**
111     * The id of the changeset this primitive was last uploaded to.
112     * 0 if it wasn't uploaded to a changeset yet of if the changeset
113     * id isn't known.
114     */
115    protected int changesetId;
116
117    protected int timestamp;
118
119    /**
120     * Get and write all attributes from the parameter. Does not fire any listener, so
121     * use this only in the data initializing phase
122     * @param other the primitive to clone data from
123     */
124    public void cloneFrom(AbstractPrimitive other) {
125        setKeys(other.getKeys());
126        id = other.id;
127        if (id <= 0) {
128            // reset version and changeset id
129            version = 0;
130            changesetId = 0;
131        }
132        timestamp = other.timestamp;
133        if (id > 0) {
134            version = other.version;
135        }
136        flags = other.flags;
137        user = other.user;
138        if (id > 0 && other.changesetId > 0) {
139            // #4208: sometimes we cloned from other with id < 0 *and*
140            // an assigned changeset id. Don't know why yet. For primitives
141            // with id < 0 we don't propagate the changeset id any more.
142            //
143            setChangesetId(other.changesetId);
144        }
145    }
146
147    /**
148     * Replies the version number as returned by the API. The version is 0 if the id is 0 or
149     * if this primitive is incomplete.
150     *
151     * @see PrimitiveData#setVersion(int)
152     */
153    @Override
154    public int getVersion() {
155        return version;
156    }
157
158    /**
159     * Replies the id of this primitive.
160     *
161     * @return the id of this primitive.
162     */
163    @Override
164    public long getId() {
165        long id = this.id;
166        return id >= 0 ? id : 0;
167    }
168
169    /**
170     * Gets a unique id representing this object.
171     *
172     * @return Osm id if primitive already exists on the server. Unique negative value if primitive is new
173     */
174    @Override
175    public long getUniqueId() {
176        return id;
177    }
178
179    /**
180     *
181     * @return True if primitive is new (not yet uploaded the server, id &lt;= 0)
182     */
183    @Override
184    public boolean isNew() {
185        return id <= 0;
186    }
187
188    /**
189     *
190     * @return True if primitive is new or undeleted
191     * @see #isNew()
192     * @see #isUndeleted()
193     */
194    @Override
195    public boolean isNewOrUndeleted() {
196        return (id <= 0) || ((flags & (FLAG_VISIBLE + FLAG_DELETED)) == 0);
197    }
198
199    /**
200     * Sets the id and the version of this primitive if it is known to the OSM API.
201     *
202     * Since we know the id and its version it can't be incomplete anymore. incomplete
203     * is set to false.
204     *
205     * @param id the id. &gt; 0 required
206     * @param version the version &gt; 0 required
207     * @throws IllegalArgumentException if id &lt;= 0
208     * @throws IllegalArgumentException if version &lt;= 0
209     * @throws DataIntegrityProblemException if id is changed and primitive was already added to the dataset
210     */
211    @Override
212    public void setOsmId(long id, int version) {
213        if (id <= 0)
214            throw new IllegalArgumentException(tr("ID > 0 expected. Got {0}.", id));
215        if (version <= 0)
216            throw new IllegalArgumentException(tr("Version > 0 expected. Got {0}.", version));
217        this.id = id;
218        this.version = version;
219        this.setIncomplete(false);
220    }
221
222    /**
223     * Clears the metadata, including id and version known to the OSM API.
224     * The id is a new unique id. The version, changeset and timestamp are set to 0.
225     * incomplete and deleted are set to false. It's preferred to use copy constructor with clearMetadata set to true instead
226     * of calling this method.
227     * @since 6140
228     */
229    public void clearOsmMetadata() {
230        // Not part of dataset - no lock necessary
231        this.id = generateUniqueId();
232        this.version = 0;
233        this.user = null;
234        this.changesetId = 0; // reset changeset id on a new object
235        this.timestamp = 0;
236        this.setIncomplete(false);
237        this.setDeleted(false);
238        this.setVisible(true);
239    }
240
241    /**
242     * Replies the user who has last touched this object. May be null.
243     *
244     * @return the user who has last touched this object. May be null.
245     */
246    @Override
247    public User getUser() {
248        return user;
249    }
250
251    /**
252     * Sets the user who has last touched this object.
253     *
254     * @param user the user
255     */
256    @Override
257    public void setUser(User user) {
258        this.user = user;
259    }
260
261    /**
262     * Replies the id of the changeset this primitive was last uploaded to.
263     * 0 if this primitive wasn't uploaded to a changeset yet or if the
264     * changeset isn't known.
265     *
266     * @return the id of the changeset this primitive was last uploaded to.
267     */
268    @Override
269    public int getChangesetId() {
270        return changesetId;
271    }
272
273    /**
274     * Sets the changeset id of this primitive. Can't be set on a new
275     * primitive.
276     *
277     * @param changesetId the id. &gt;= 0 required.
278     * @throws IllegalStateException if this primitive is new.
279     * @throws IllegalArgumentException if id &lt; 0
280     */
281    @Override
282    public void setChangesetId(int changesetId) {
283        if (this.changesetId == changesetId)
284            return;
285        if (changesetId < 0)
286            throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' >= 0 expected, got {1}", "changesetId", changesetId));
287        if (isNew() && changesetId > 0)
288            throw new IllegalStateException(tr("Cannot assign a changesetId > 0 to a new primitive. Value of changesetId is {0}", changesetId));
289
290        this.changesetId = changesetId;
291    }
292
293    /**
294     * Replies the unique primitive id for this primitive
295     *
296     * @return the unique primitive id for this primitive
297     */
298    @Override
299    public PrimitiveId getPrimitiveId() {
300        return new SimplePrimitiveId(getUniqueId(), getType());
301    }
302
303    public OsmPrimitiveType getDisplayType() {
304        return getType();
305    }
306
307    @Override
308    public void setTimestamp(Date timestamp) {
309        this.timestamp = (int) (timestamp.getTime() / 1000);
310    }
311
312    @Override
313    public void setRawTimestamp(int timestamp) {
314        this.timestamp = timestamp;
315    }
316
317    /**
318     * Time of last modification to this object. This is not set by JOSM but
319     * read from the server and delivered back to the server unmodified. It is
320     * used to check against edit conflicts.
321     *
322     * @return date of last modification
323     */
324    @Override
325    public Date getTimestamp() {
326        return new Date(timestamp * 1000L);
327    }
328
329    @Override
330    public int getRawTimestamp() {
331        return timestamp;
332    }
333
334    @Override
335    public boolean isTimestampEmpty() {
336        return timestamp == 0;
337    }
338
339    /* -------
340    /* FLAGS
341    /* ------*/
342
343    protected void updateFlags(int flag, boolean value) {
344        if (value) {
345            flags |= flag;
346        } else {
347            flags &= ~flag;
348        }
349    }
350
351    /**
352     * Marks this primitive as being modified.
353     *
354     * @param modified true, if this primitive is to be modified
355     */
356    @Override
357    public void setModified(boolean modified) {
358        updateFlags(FLAG_MODIFIED, modified);
359    }
360
361    /**
362     * Replies <code>true</code> if the object has been modified since it was loaded from
363     * the server. In this case, on next upload, this object will be updated.
364     *
365     * Deleted objects are deleted from the server. If the objects are added (id=0),
366     * the modified is ignored and the object is added to the server.
367     *
368     * @return <code>true</code> if the object has been modified since it was loaded from
369     * the server
370     */
371    @Override
372    public boolean isModified() {
373        return (flags & FLAG_MODIFIED) != 0;
374    }
375
376    /**
377     * Replies <code>true</code>, if the object has been deleted.
378     *
379     * @return <code>true</code>, if the object has been deleted.
380     * @see #setDeleted(boolean)
381     */
382    @Override
383    public boolean isDeleted() {
384        return (flags & FLAG_DELETED) != 0;
385    }
386
387    /**
388     * Replies <code>true</code> if the object has been deleted on the server and was undeleted by the user.
389     * @return <code>true</code> if the object has been undeleted
390     */
391    public boolean isUndeleted() {
392        return (flags & (FLAG_VISIBLE + FLAG_DELETED)) == 0;
393    }
394
395    /**
396     * Replies <code>true</code>, if the object is usable
397     * (i.e. complete and not deleted).
398     *
399     * @return <code>true</code>, if the object is usable.
400     * @see #setDeleted(boolean)
401     */
402    public boolean isUsable() {
403        return (flags & (FLAG_DELETED + FLAG_INCOMPLETE)) == 0;
404    }
405
406    /**
407     * Checks if object is known to the server.
408     * Replies true if this primitive is either unknown to the server (i.e. its id
409     * is 0) or it is known to the server and it hasn't be deleted on the server.
410     * Replies false, if this primitive is known on the server and has been deleted
411     * on the server.
412     *
413     * @return <code>true</code>, if the object is visible on server.
414     * @see #setVisible(boolean)
415     */
416    @Override
417    public boolean isVisible() {
418        return (flags & FLAG_VISIBLE) != 0;
419    }
420
421    /**
422     * Sets whether this primitive is visible, i.e. whether it is known on the server
423     * and not deleted on the server.
424     *
425     * @see #isVisible()
426     * @throws IllegalStateException if visible is set to false on an primitive with id==0
427     */
428    @Override
429    public void setVisible(boolean visible) {
430        if (isNew() && !visible)
431            throw new IllegalStateException(tr("A primitive with ID = 0 cannot be invisible."));
432        updateFlags(FLAG_VISIBLE, visible);
433    }
434
435    /**
436     * Sets whether this primitive is deleted or not.
437     *
438     * Also marks this primitive as modified if deleted is true.
439     *
440     * @param deleted  true, if this primitive is deleted; false, otherwise
441     */
442    @Override
443    public void setDeleted(boolean deleted) {
444        updateFlags(FLAG_DELETED, deleted);
445        setModified(deleted ^ !isVisible());
446    }
447
448    /**
449     * If set to true, this object is incomplete, which means only the id
450     * and type is known (type is the objects instance class)
451     * @param incomplete incomplete flag value
452     */
453    protected void setIncomplete(boolean incomplete) {
454        updateFlags(FLAG_INCOMPLETE, incomplete);
455    }
456
457    @Override
458    public boolean isIncomplete() {
459        return (flags & FLAG_INCOMPLETE) != 0;
460    }
461
462    protected String getFlagsAsString() {
463        StringBuilder builder = new StringBuilder();
464
465        if (isIncomplete()) {
466            builder.append('I');
467        }
468        if (isModified()) {
469            builder.append('M');
470        }
471        if (isVisible()) {
472            builder.append('V');
473        }
474        if (isDeleted()) {
475            builder.append('D');
476        }
477        return builder.toString();
478    }
479
480    /*------------
481     * Keys handling
482     ------------*/
483
484    /**
485     * The key/value list for this primitive.
486     * <p>
487     * Note that the keys field is synchronized using RCU.
488     * Writes to it are not synchronized by this object, the writers have to synchronize writes themselves.
489     * <p>
490     * In short this means that you should not rely on this variable being the same value when read again and your should always
491     * copy it on writes.
492     * <p>
493     * Further reading:
494     * <ul>
495     * <li>{@link java.util.concurrent.CopyOnWriteArrayList}</li>
496     * <li> <a href="http://stackoverflow.com/questions/2950871/how-can-copyonwritearraylist-be-thread-safe">
497     *     http://stackoverflow.com/questions/2950871/how-can-copyonwritearraylist-be-thread-safe</a></li>
498     * <li> <a href="https://en.wikipedia.org/wiki/Read-copy-update">
499     *     https://en.wikipedia.org/wiki/Read-copy-update</a> (mind that we have a Garbage collector,
500     *     {@code rcu_assign_pointer} and {@code rcu_dereference} are ensured by the {@code volatile} keyword)</li>
501     * </ul>
502     */
503    protected volatile String[] keys;
504
505    /**
506     * Replies the map of key/value pairs. Never replies null. The map can be empty, though.
507     *
508     * @return tags of this primitive. Changes made in returned map are not mapped
509     * back to the primitive, use setKeys() to modify the keys
510     * @see #visitKeys(KeyValueVisitor)
511     */
512    @Override
513    public Map<String, String> getKeys() {
514        String[] keys = this.keys;
515        final Map<String, String> result = new HashMap<>(
516                Utils.hashMapInitialCapacity(keys == null ? 0 : keys.length / 2));
517        if (keys != null) {
518            for (int i = 0; i < keys.length; i += 2) {
519                result.put(keys[i], keys[i + 1]);
520            }
521        }
522        return result;
523    }
524
525    /**
526     * Calls the visitor for every key/value pair of this primitive.
527     *
528     * @param visitor The visitor to call.
529     * @see #getKeys()
530     * @since 8742
531     */
532    public void visitKeys(KeyValueVisitor visitor) {
533        final String[] keys = this.keys;
534        if (keys != null) {
535            for (int i = 0; i < keys.length; i += 2) {
536                visitor.visitKeyValue(this, keys[i], keys[i + 1]);
537            }
538        }
539    }
540
541    /**
542     * Sets the keys of this primitives to the key/value pairs in <code>keys</code>.
543     * Old key/value pairs are removed.
544     * If <code>keys</code> is null, clears existing key/value pairs.
545     * <p>
546     * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used
547     * from multiple threads.
548     *
549     * @param keys the key/value pairs to set. If null, removes all existing key/value pairs.
550     */
551    @Override
552    public void setKeys(Map<String, String> keys) {
553        Map<String, String> originalKeys = getKeys();
554        if (keys == null || keys.isEmpty()) {
555            this.keys = null;
556            keysChangedImpl(originalKeys);
557            return;
558        }
559        String[] newKeys = new String[keys.size() * 2];
560        int index = 0;
561        for (Entry<String, String> entry:keys.entrySet()) {
562            newKeys[index++] = entry.getKey();
563            newKeys[index++] = entry.getValue();
564        }
565        this.keys = newKeys;
566        keysChangedImpl(originalKeys);
567    }
568
569    /**
570     * Set the given value to the given key. If key is null, does nothing. If value is null,
571     * removes the key and behaves like {@link #remove(String)}.
572     * <p>
573     * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used
574     * from multiple threads.
575     *
576     * @param key  The key, for which the value is to be set. Can be null or empty, does nothing in this case.
577     * @param value The value for the key. If null, removes the respective key/value pair.
578     *
579     * @see #remove(String)
580     */
581    @Override
582    public void put(String key, String value) {
583        Map<String, String> originalKeys = getKeys();
584        if (key == null || Utils.strip(key).isEmpty())
585            return;
586        else if (value == null) {
587            remove(key);
588        } else if (keys == null) {
589            keys = new String[] {key, value};
590            keysChangedImpl(originalKeys);
591        } else {
592            int keyIndex = indexOfKey(keys, key);
593            int tagArrayLength = keys.length;
594            if (keyIndex < 0) {
595                keyIndex = tagArrayLength;
596                tagArrayLength += 2;
597            }
598
599            // Do not try to optimize this array creation if the key already exists.
600            // We would need to convert the keys array to be an AtomicReferenceArray
601            // Or we would at least need a volatile write after the array was modified to
602            // ensure that changes are visible by other threads.
603            String[] newKeys = Arrays.copyOf(keys, tagArrayLength);
604            newKeys[keyIndex] = key;
605            newKeys[keyIndex + 1] = value;
606            keys = newKeys;
607            keysChangedImpl(originalKeys);
608        }
609    }
610
611    /**
612     * Scans a key/value array for a given key.
613     * @param keys The key array. It is not modified. It may be null to indicate an emtpy array.
614     * @param key The key to search for.
615     * @return The position of that key in the keys array - which is always a multiple of 2 - or -1 if it was not found.
616     */
617    private static int indexOfKey(String[] keys, String key) {
618        if (keys == null) {
619            return -1;
620        }
621        for (int i = 0; i < keys.length; i += 2) {
622            if (keys[i].equals(key)) {
623                return i;
624            }
625        }
626        return -1;
627    }
628
629    /**
630     * Remove the given key from the list
631     * <p>
632     * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used
633     * from multiple threads.
634     *
635     * @param key  the key to be removed. Ignored, if key is null.
636     */
637    @Override
638    public void remove(String key) {
639        if (key == null || keys == null) return;
640        if (!hasKey(key))
641            return;
642        Map<String, String> originalKeys = getKeys();
643        if (keys.length == 2) {
644            keys = null;
645            keysChangedImpl(originalKeys);
646            return;
647        }
648        String[] newKeys = new String[keys.length - 2];
649        int j = 0;
650        for (int i = 0; i < keys.length; i += 2) {
651            if (!keys[i].equals(key)) {
652                newKeys[j++] = keys[i];
653                newKeys[j++] = keys[i+1];
654            }
655        }
656        keys = newKeys;
657        keysChangedImpl(originalKeys);
658    }
659
660    /**
661     * Removes all keys from this primitive.
662     * <p>
663     * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used
664     * from multiple threads.
665     */
666    @Override
667    public void removeAll() {
668        if (keys != null) {
669            Map<String, String> originalKeys = getKeys();
670            keys = null;
671            keysChangedImpl(originalKeys);
672        }
673    }
674
675    /**
676     * Replies the value for key <code>key</code>. Replies null, if <code>key</code> is null.
677     * Replies null, if there is no value for the given key.
678     *
679     * @param key the key. Can be null, replies null in this case.
680     * @return the value for key <code>key</code>.
681     */
682    @Override
683    public final String get(String key) {
684        String[] keys = this.keys;
685        if (key == null)
686            return null;
687        if (keys == null)
688            return null;
689        for (int i = 0; i < keys.length; i += 2) {
690            if (keys[i].equals(key)) return keys[i+1];
691        }
692        return null;
693    }
694
695    /**
696     * Returns true if the {@code key} corresponds to an OSM true value.
697     * @param key OSM key
698     * @return {@code true} if the {@code key} corresponds to an OSM true value
699     * @see OsmUtils#isTrue(String)
700     */
701    public final boolean isKeyTrue(String key) {
702        return OsmUtils.isTrue(get(key));
703    }
704
705    /**
706     * Returns true if the {@code key} corresponds to an OSM false value.
707     * @param key OSM key
708     * @return {@code true} if the {@code key} corresponds to an OSM false value
709     * @see OsmUtils#isFalse(String)
710     */
711    public final boolean isKeyFalse(String key) {
712        return OsmUtils.isFalse(get(key));
713    }
714
715    public final String getIgnoreCase(String key) {
716        String[] keys = this.keys;
717        if (key == null)
718            return null;
719        if (keys == null)
720            return null;
721        for (int i = 0; i < keys.length; i += 2) {
722            if (keys[i].equalsIgnoreCase(key)) return keys[i+1];
723        }
724        return null;
725    }
726
727    public final int getNumKeys() {
728        String[] keys = this.keys;
729        return keys == null ? 0 : keys.length / 2;
730    }
731
732    @Override
733    public final Collection<String> keySet() {
734        final String[] keys = this.keys;
735        if (keys == null) {
736            return Collections.emptySet();
737        }
738        if (keys.length == 1) {
739            return Collections.singleton(keys[0]);
740        }
741
742        final Set<String> result = new HashSet<>(Utils.hashMapInitialCapacity(keys.length / 2));
743        for (int i = 0; i < keys.length; i += 2) {
744            result.add(keys[i]);
745        }
746        return result;
747    }
748
749    /**
750     * Replies true, if the map of key/value pairs of this primitive is not empty.
751     *
752     * @return true, if the map of key/value pairs of this primitive is not empty; false
753     *   otherwise
754     */
755    @Override
756    public final boolean hasKeys() {
757        return keys != null;
758    }
759
760    /**
761     * Replies true if this primitive has a tag with key <code>key</code>.
762     *
763     * @param key the key
764     * @return true, if his primitive has a tag with key <code>key</code>
765     */
766    public boolean hasKey(String key) {
767        return key != null && indexOfKey(keys, key) >= 0;
768    }
769
770    /**
771     * What to do, when the tags have changed by one of the tag-changing methods.
772     * @param originalKeys original tags
773     */
774    protected abstract void keysChangedImpl(Map<String, String> originalKeys);
775
776    /**
777     * Replies the name of this primitive. The default implementation replies the value
778     * of the tag <tt>name</tt> or null, if this tag is not present.
779     *
780     * @return the name of this primitive
781     */
782    @Override
783    public String getName() {
784        return get("name");
785    }
786
787    /**
788     * Replies a localized name for this primitive given by the value of the name tags
789     * accessed from very specific (language variant) to more generic (default name).
790     *
791     * @see LanguageInfo#getLanguageCodes
792     * @return the name of this primitive, <code>null</code> if no name exists
793     */
794    @Override
795    public String getLocalName() {
796        for (String s : LanguageInfo.getLanguageCodes(null)) {
797            String val = get("name:" + s);
798            if (val != null)
799                return val;
800        }
801
802        return getName();
803    }
804
805    /**
806     * Tests whether this primitive contains a tag consisting of {@code key} and {@code values}.
807     * @param key the key forming the tag.
808     * @param value value forming the tag.
809     * @return true iff primitive contains a tag consisting of {@code key} and {@code value}.
810     */
811    public boolean hasTag(String key, String value) {
812        return Objects.equals(value, get(key));
813    }
814
815    /**
816     * Tests whether this primitive contains a tag consisting of {@code key} and any of {@code values}.
817     * @param key the key forming the tag.
818     * @param values one or many values forming the tag.
819     * @return true if primitive contains a tag consisting of {@code key} and any of {@code values}.
820     */
821    public boolean hasTag(String key, String... values) {
822        return hasTag(key, Arrays.asList(values));
823    }
824
825    /**
826     * Tests whether this primitive contains a tag consisting of {@code key} and any of {@code values}.
827     * @param key the key forming the tag.
828     * @param values one or many values forming the tag.
829     * @return true iff primitive contains a tag consisting of {@code key} and any of {@code values}.
830     */
831    public boolean hasTag(String key, Collection<String> values) {
832        return values.contains(get(key));
833    }
834}