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 <= 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. > 0 required 206 * @param version the version > 0 required 207 * @throws IllegalArgumentException if id <= 0 208 * @throws IllegalArgumentException if version <= 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. >= 0 required. 278 * @throws IllegalStateException if this primitive is new. 279 * @throws IllegalArgumentException if id < 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}