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