source: josm/trunk/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java@ 12463

Last change on this file since 12463 was 12190, checked in by michael2402, 7 years ago

See #14794: More javadoc for data.osm package

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