1 package org.unicode.cldr.util; 2 3 import java.sql.Timestamp; 4 import java.util.ArrayList; 5 import java.util.Collections; 6 import java.util.Comparator; 7 import java.util.Date; 8 import java.util.EnumMap; 9 import java.util.EnumSet; 10 import java.util.HashMap; 11 import java.util.Iterator; 12 import java.util.LinkedHashMap; 13 import java.util.LinkedHashSet; 14 import java.util.List; 15 import java.util.Map; 16 import java.util.Map.Entry; 17 import java.util.Set; 18 import java.util.TreeMap; 19 import java.util.TreeSet; 20 import java.util.regex.Matcher; 21 import java.util.regex.Pattern; 22 23 import org.unicode.cldr.test.CheckWidths; 24 import org.unicode.cldr.test.DisplayAndInputProcessor; 25 import org.unicode.cldr.util.VettingViewer.VoteStatus; 26 27 import com.google.common.base.Objects; 28 import com.google.common.collect.ImmutableSet; 29 import com.ibm.icu.text.Collator; 30 import com.ibm.icu.util.ULocale; 31 32 /** 33 * This class implements the vote resolution process agreed to by the CLDR 34 * committee. Here is an example of usage: 35 * 36 * <pre> 37 * // before doing anything, initialize the voter data (who are the voters at what levels) with setVoterToInfo. 38 * // We assume this doesn't change often 39 * // here is some fake data: 40 * VoteResolver.setVoterToInfo(Utility.asMap(new Object[][] { 41 * { 666, new VoterInfo(Organization.google, Level.vetter, "J. Smith") }, 42 * { 555, new VoterInfo(Organization.google, Level.street, "S. Jones") }, 43 * { 444, new VoterInfo(Organization.google, Level.vetter, "S. Samuels") }, 44 * { 333, new VoterInfo(Organization.apple, Level.vetter, "A. Mutton") }, 45 * { 222, new VoterInfo(Organization.adobe, Level.expert, "A. Aldus") }, 46 * { 111, new VoterInfo(Organization.ibm, Level.street, "J. Henry") }, })); 47 * 48 * // you can create a resolver and keep it around. It isn't thread-safe, so either have a separate one per thread (they 49 * // are small), or synchronize. 50 * VoteResolver resolver = new VoteResolver(); 51 * 52 * // For any particular base path, set the values 53 * // set the 1.5 status (if we're working on 1.6). This <b>must</b> be done for each new base path 54 * resolver.newPath(oldValue, oldStatus); 55 * [TODO: function newPath doesn't exist, revise this documentation] 56 * 57 * // now add some values, with who voted for them 58 * resolver.add(value1, voter1); 59 * resolver.add(value1, voter2); 60 * resolver.add(value2, voter3); 61 * 62 * // Once you've done that, you can get the results for the base path 63 * winner = resolver.getWinningValue(); 64 * status = resolver.getWinningStatus(); 65 * conflicts = resolver.getConflictedOrganizations(); 66 * </pre> 67 */ 68 public class VoteResolver<T> { 69 private static final boolean DEBUG = false; 70 71 /** 72 * A placeholder for winningValue when it would otherwise be null. 73 * It must match NO_WINNING_VALUE in the client JavaScript code. 74 */ 75 private static String NO_WINNING_VALUE = "no-winning-value"; 76 77 /** 78 * The status levels according to the committee, in ascending order 79 * 80 * Status corresponds to icons as follows: 81 * A checkmark means it’s approved and is slated to be used. A cross means it’s a missing value. 82 * Green/orange check: The item has enough votes to be used in CLDR. 83 * Red/orange/black X: The item does not have enough votes to be used in CLDR, by most implementations (or is completely missing). 84 * Reference: http://cldr.unicode.org/translation/getting-started/guide 85 * 86 * New January, 2019: When the item is inherited, i.e., winningValue is INHERITANCE_MARKER (↑↑↑), 87 * then orange/red X are replaced by orange/red up-arrow. That change is made only on the client. 88 * Reference: https://unicode.org/cldr/trac/ticket/11103 89 * 90 * Status.approved: approved.png = green check 91 * Status.contributed: contributed.png = orange check 92 * Status.provisional: provisional.png = orange X (or inherited_provisional.png orange up-arrow if inherited) 93 * Status.unconfirmed: unconfirmed.png = red X (or inherited_unconfirmed.png red up-arrow if inherited 94 * Status.missing: missing.png = black X 95 */ 96 public enum Status { 97 missing, unconfirmed, provisional, contributed, approved; fromString(String source)98 public static Status fromString(String source) { 99 return source == null ? missing : Status.valueOf(source); 100 } 101 } 102 103 /** 104 * This is the "high bar" level where flagging is required. 105 * @see #getRequiredVotes() 106 */ 107 public static final int HIGH_BAR = Level.tc.votes; 108 109 /** 110 * This is the level at which a vote counts. Each level also contains the 111 * weight. 112 */ 113 public enum Level { 114 locked( 0 /* votes */, 999 /* stlevel */), 115 street( 1 /* votes */, 10 /* stlevel */), 116 anonymous(0 /* votes */, 8 /* stlevel */), 117 vetter( 4 /* votes */, 5 /* stlevel */), 118 expert( 8 /* votes */, 3 /* stlevel */), 119 manager( 4 /* votes */, 2 /* stlevel */), 120 tc( 20 /* votes */, 1 /* stlevel */), 121 admin( 100 /* votes */, 0 /* stlevel */); 122 123 /** 124 * PERMANENT_VOTES is used by TC voters to "lock" locale+path permanently (including future versions, until unlocked), 125 * in the current VOTE_VALUE table. It is public for STFactory.java and PermanentVote.java. 126 */ 127 public static final int PERMANENT_VOTES = 1000; 128 129 /** 130 * LOCKING_VOTES is used (nominally by ADMIN voter, but not really by someone logged in as ADMIN, instead 131 * by combination of two PERMANENT_VOTES) to "lock" locale+path permanently in the LOCKED_XPATHS table. 132 * It is public for STFactory.PerLocaleData.loadVoteValues. 133 */ 134 public static final int LOCKING_VOTES = 2000; 135 136 /** 137 * The vote count a user of this level normally votes with 138 */ 139 private final int votes; 140 141 /** 142 * The level as an integer, where 0 = admin, ..., 999 = locked 143 */ 144 private final int stlevel; 145 Level(int votes, int stlevel)146 private Level(int votes, int stlevel) { 147 this.votes = votes; 148 this.stlevel = stlevel; 149 } 150 151 /** 152 * Get the votes for each level 153 */ getVotes()154 public int getVotes() { 155 return votes; 156 } 157 158 /** 159 * Get the Survey Tool userlevel for each level. (0=admin, 999=locked) 160 */ getSTLevel()161 public int getSTLevel() { 162 return stlevel; 163 } 164 165 /** 166 * Find the Level, given ST Level 167 * 168 * @param stlevel 169 * @return the Level corresponding to the integer 170 */ fromSTLevel(int stlevel)171 public static Level fromSTLevel(int stlevel) { 172 for (Level l : Level.values()) { 173 if (l.getSTLevel() == stlevel) { 174 return l; 175 } 176 } 177 return null; 178 } 179 180 /** 181 * Policy: can this user manage the "other" user's settings? 182 * 183 * @param myOrg 184 * the current organization 185 * @param otherLevel 186 * the other user's level 187 * @param otherOrg 188 * the other user's organization 189 * @return 190 */ isManagerFor(Organization myOrg, Level otherLevel, Organization otherOrg)191 public boolean isManagerFor(Organization myOrg, Level otherLevel, Organization otherOrg) { 192 return (this == admin || (canManageSomeUsers() && 193 (myOrg == otherOrg) && this.getSTLevel() <= otherLevel.getSTLevel())); 194 } 195 196 /** 197 * Policy: Can this user manage any users? 198 * 199 * @return 200 */ canManageSomeUsers()201 public boolean canManageSomeUsers() { 202 return this.getSTLevel() <= manager.getSTLevel(); 203 } 204 205 /** 206 * Policy: can this user create or set a user to the specified level? 207 */ canCreateOrSetLevelTo(Level otherLevel)208 public boolean canCreateOrSetLevelTo(Level otherLevel) { 209 return (this == admin) || // admin can set any level 210 (otherLevel != expert && // expert can't be set by any users but admin 211 canManageSomeUsers() && // must be some sort of manager 212 otherLevel.getSTLevel() >= getSTLevel()); // can't gain higher privs 213 } 214 215 /** 216 * Can a user with this level vote with the given vote count? 217 * 218 * @param withVotes the given vote count 219 * @return true if the user can vote with the given vote count, else false 220 */ canVoteWithCount(int withVotes)221 public boolean canVoteWithCount(int withVotes) { 222 /* 223 * ADMIN is allowed to vote with LOCKING_VOTES, but not directly in the GUI, only 224 * by two TC voting together with PERMANENT_VOTES. Therefore LOCKING_VOTES is omitted 225 * from the GUI menu (voteCountMenu), but included in canVoteWithCount. 226 */ 227 if (withVotes == LOCKING_VOTES && this == admin) { 228 return true; 229 } 230 Set<Integer> menu = getVoteCountMenu(); 231 return menu == null ? withVotes == this.votes : menu.contains(withVotes); 232 } 233 234 /** 235 * If not null, an array of different vote counts from which a user of this 236 * level is allowed to choose. 237 */ 238 private ImmutableSet<Integer> voteCountMenu = null; 239 240 /** 241 * Get the ordered immutable set of different vote counts a user of this level can vote with 242 * 243 * @return the set, or null if the user has no choice of vote count 244 */ getVoteCountMenu()245 public ImmutableSet<Integer> getVoteCountMenu() { 246 return voteCountMenu; 247 } 248 249 /* 250 * Set voteCountMenu for admin and tc in this static block, which will be run after 251 * all the constructors have run, rather than in the constructor itself. For example, 252 * vetter.votes needs to be defined before we can set admin.voteCountMenu. 253 */ 254 static { 255 admin.voteCountMenu = ImmutableSet.of(vetter.votes, admin.votes); /* Not LOCKING_VOTES; see canVoteWithCount */ 256 tc.voteCountMenu = ImmutableSet.of(vetter.votes, tc.votes, PERMANENT_VOTES); 257 } 258 } 259 260 /** 261 * Internal class for voter information. It is public for testing only 262 */ 263 public static class VoterInfo { 264 private Organization organization; 265 private Level level; 266 private String name; 267 private Set<String> locales = new TreeSet<>(); 268 VoterInfo(Organization organization, Level level, String name, Set<String> locales)269 public VoterInfo(Organization organization, Level level, String name, Set<String> locales) { 270 this.setOrganization(organization); 271 this.setLevel(level); 272 this.setName(name); 273 this.locales.addAll(locales); 274 } 275 VoterInfo(Organization organization, Level level, String name)276 public VoterInfo(Organization organization, Level level, String name) { 277 this.setOrganization(organization); 278 this.setLevel(level); 279 this.setName(name); 280 } 281 VoterInfo()282 public VoterInfo() { 283 } 284 285 @Override toString()286 public String toString() { 287 return "{" + getName() + ", " + getLevel() + ", " + getOrganization() + "}"; 288 } 289 setOrganization(Organization organization)290 public void setOrganization(Organization organization) { 291 this.organization = organization; 292 } 293 getOrganization()294 public Organization getOrganization() { 295 return organization; 296 } 297 setLevel(Level level)298 public void setLevel(Level level) { 299 this.level = level; 300 } 301 getLevel()302 public Level getLevel() { 303 return level; 304 } 305 setName(String name)306 public void setName(String name) { 307 this.name = name; 308 } 309 getName()310 public String getName() { 311 return name; 312 } 313 setLocales(Set<String> locales)314 public void setLocales(Set<String> locales) { 315 this.locales = locales; 316 } 317 addLocales(Set<String> locales)318 public void addLocales(Set<String> locales) { 319 this.locales.addAll(locales); 320 } 321 getLocales()322 public Set<String> getLocales() { 323 return locales; 324 } 325 addLocale(String locale)326 public void addLocale(String locale) { 327 this.locales.add(locale); 328 } 329 330 @Override equals(Object obj)331 public boolean equals(Object obj) { 332 if (obj == null) { 333 return false; 334 } 335 VoterInfo other = (VoterInfo) obj; 336 return organization.equals(other.organization) 337 && level.equals(other.level) 338 && name.equals(other.name) 339 && Objects.equal(locales, other.locales); 340 } 341 342 @Override hashCode()343 public int hashCode() { 344 return organization.hashCode() ^ level.hashCode() ^ name.hashCode(); 345 } 346 } 347 348 /** 349 * MaxCounter: make sure that we are always only getting the maximum of the values. 350 * 351 * @author markdavis 352 * 353 * @param <T> 354 */ 355 static class MaxCounter<T> extends Counter<T> { MaxCounter(boolean b)356 public MaxCounter(boolean b) { 357 super(b); 358 } 359 360 /** 361 * Add, but only to bring up to the maximum value. 362 */ 363 @Override add(T obj, long countValue, long time)364 public MaxCounter<T> add(T obj, long countValue, long time) { 365 long value = getCount(obj); 366 if ((value <= countValue)) { 367 super.add(obj, countValue - value, time); // only add the difference! 368 } 369 return this; 370 } 371 } 372 373 /** 374 * Internal class for getting from an organization to its vote. 375 */ 376 private static class OrganizationToValueAndVote<T> { 377 private final Map<Organization, MaxCounter<T>> orgToVotes = new EnumMap<>(Organization.class); 378 private final Counter<T> totalVotes = new Counter<>(); 379 private final Map<Organization, Integer> orgToMax = new EnumMap<>(Organization.class); 380 private final Counter<T> totals = new Counter<>(true); 381 private Map<String, Long> nameTime = new LinkedHashMap<>(); 382 // map an organization to what it voted for. 383 private final Map<Organization, T> orgToAdd = new EnumMap<>(Organization.class); 384 private T baileyValue; 385 private boolean baileySet; // was the bailey value set 386 OrganizationToValueAndVote()387 OrganizationToValueAndVote() { 388 for (Organization org : Organization.values()) { 389 orgToVotes.put(org, new MaxCounter<T>(true)); 390 } 391 } 392 393 /** 394 * Call clear before considering each new path 395 */ clear()396 public void clear() { 397 for (Map.Entry<Organization, MaxCounter<T>> entry : orgToVotes.entrySet()) { 398 // for (Organization org : orgToVotes.keySet()) { 399 // orgToVotes.get(org).clear(); 400 entry.getValue().clear(); 401 } 402 orgToAdd.clear(); 403 orgToMax.clear(); 404 totalVotes.clear(); 405 baileyValue = null; 406 baileySet = false; 407 } 408 countValuesWithVotes()409 public int countValuesWithVotes() { 410 return totalVotes.size(); 411 } 412 413 /** 414 * Returns value of voted item, in case there is exactly 1. 415 * 416 * @return 417 */ getSingleVotedItem()418 public T getSingleVotedItem() { 419 return totalVotes.size() != 1 ? null : totalVotes.iterator().next(); 420 } 421 getNameTime()422 public Map<String, Long> getNameTime() { 423 return nameTime; 424 } 425 426 /** 427 * Call this to add votes 428 * 429 * @param value 430 * @param voter 431 * @param withVotes optionally, vote at a non-typical voting level. May not exceed voter's maximum allowed level. null = use default level. 432 * @param date 433 */ add(T value, int voter, Integer withVotes, Date date)434 public void add(T value, int voter, Integer withVotes, Date date) { 435 final VoterInfo info = getVoterToInfo().get(voter); 436 if (info == null) { 437 throw new UnknownVoterException(voter); 438 } 439 Level level = info.getLevel(); 440 if (withVotes == null || !level.canVoteWithCount(withVotes)) { 441 withVotes = level.getVotes(); 442 } 443 addInternal(value, info, withVotes, date); // do the add 444 } 445 446 /** 447 * Called by add(T,int,Integer) to actually add a value. 448 * 449 * @param value 450 * @param info 451 * @param votes 452 * @param date 453 * @see #add(Object, int, Integer) 454 */ addInternal(T value, final VoterInfo info, final int votes, Date time)455 private void addInternal(T value, final VoterInfo info, final int votes, Date time) { 456 if (baileySet == false) { 457 throw new IllegalArgumentException("setBaileyValue must be called before add"); 458 } 459 totalVotes.add(value, votes, time.getTime()); 460 nameTime.put(info.getName(), time.getTime()); 461 if (DEBUG) { 462 System.out.println("totalVotes Info: " + totalVotes.toString()); 463 } 464 if (DEBUG) { 465 System.out.println("VoteInfo: " + info.getName() + info.getOrganization()); 466 } 467 Organization organization = info.getOrganization(); 468 //orgToVotes.get(organization).clear(); 469 orgToVotes.get(organization).add(value, votes, time.getTime()); 470 if (DEBUG) { 471 System.out.println("Adding now Info: " + organization.displayName + info.getName() + " is adding: " + votes + value 472 + new Timestamp(time.getTime()).toString()); 473 } 474 475 if (DEBUG) { 476 System.out.println("addInternal: " + organization.displayName + " : " + orgToVotes.get(organization).toString()); 477 } 478 479 // add the new votes to orgToMax, if they are greater that what was there 480 Integer max = orgToMax.get(info.getOrganization()); 481 if (max == null || max < votes) { 482 orgToMax.put(organization, votes); 483 } 484 } 485 486 /** 487 * Return the overall vote for each organization. It is the max for each value. 488 * When the organization is conflicted (the top two values have the same vote), the organization is also added 489 * to disputed. 490 * 491 * @param conflictedOrganizations if not null, to be filled in with the set of conflicted organizations. 492 */ getTotals(EnumSet<Organization> conflictedOrganizations)493 public Counter<T> getTotals(EnumSet<Organization> conflictedOrganizations) { 494 if (conflictedOrganizations != null) { 495 conflictedOrganizations.clear(); 496 } 497 totals.clear(); 498 499 for (Map.Entry<Organization, MaxCounter<T>> entry : orgToVotes.entrySet()) { 500 Counter<T> items = entry.getValue(); 501 if (items.size() == 0) { 502 continue; 503 } 504 Iterator<T> iterator = items.getKeysetSortedByCount(false).iterator(); 505 T value = iterator.next(); 506 long weight = items.getCount(value); 507 if (weight == 0) { 508 continue; 509 } 510 Organization org = entry.getKey(); 511 if (DEBUG) { 512 System.out.println("sortedKeys?? " + value + " " + org.displayName); 513 } 514 515 // if there is more than one item, check that it is less 516 if (iterator.hasNext()) { 517 T value2 = iterator.next(); 518 long weight2 = items.getCount(value2); 519 // if the votes for #1 are not better than #2, we have a dispute 520 if (weight == weight2) { 521 if (conflictedOrganizations != null) { 522 conflictedOrganizations.add(org); 523 } 524 } 525 } 526 // This is deprecated, but preserve it until the method is removed. 527 /* 528 * TODO: explain the above comment, and follow through. What is deprecated (orgToAdd, or getOrgVote)? 529 * Preserve until which method is removed (getOrgVote)? 530 */ 531 orgToAdd.put(org, value); 532 533 // We add the max vote for each of the organizations choices 534 long maxCount = 0; 535 T considerItem = null; 536 long considerCount = 0; 537 long maxtime = 0; 538 long considerTime = 0; 539 for (T item : items.keySet()) { 540 if (DEBUG) { 541 System.out.println("Items in order: " + item.toString() + new Timestamp(items.getTime(item)).toString()); 542 } 543 long count = items.getCount(item); 544 long time = items.getTime(item); 545 if (count > maxCount) { 546 maxCount = count; 547 maxtime = time; 548 considerItem = item; 549 if (DEBUG) { 550 System.out.println("count>maxCount: " + considerItem.toString() + ":" + new Timestamp(considerTime).toString() + " COUNT: " 551 + considerCount + "MAXCOUNT: " + maxCount); 552 } 553 considerCount = items.getCount(considerItem); 554 considerTime = items.getTime(considerItem); 555 556 } else if ((time > maxtime) && (count == maxCount)) { 557 maxCount = count; 558 maxtime = time; 559 considerItem = item; 560 considerCount = items.getCount(considerItem); 561 considerTime = items.getTime(considerItem); 562 if (DEBUG) { 563 System.out.println("time>maxTime: " + considerItem.toString() + ":" + new Timestamp(considerTime).toString()); 564 } 565 } 566 } 567 orgToAdd.put(org, considerItem); 568 totals.add(considerItem, considerCount, considerTime); 569 570 if (DEBUG) { 571 System.out.println("Totals: " + totals.toString() + " : " + new Timestamp(considerTime).toString()); 572 } 573 574 } 575 576 if (DEBUG) { 577 System.out.println("FINALTotals: " + totals.toString()); 578 } 579 return totals; 580 } 581 getOrgCount(T winningValue)582 public int getOrgCount(T winningValue) { 583 int orgCount = 0; 584 for (Map.Entry<Organization, MaxCounter<T>> entry : orgToVotes.entrySet()) { 585 Counter<T> counter = entry.getValue(); 586 long count = counter.getCount(winningValue); 587 if (count > 0) { 588 orgCount++; 589 } 590 } 591 return orgCount; 592 } 593 getBestPossibleVote()594 public int getBestPossibleVote() { 595 int total = 0; 596 for (Map.Entry<Organization, Integer> entry : orgToMax.entrySet()) { 597 total += entry.getValue(); 598 } 599 return total; 600 } 601 602 @Override toString()603 public String toString() { 604 String orgToVotesString = ""; 605 for (Entry<Organization, MaxCounter<T>> entry : orgToVotes.entrySet()) { 606 Counter<T> counter = entry.getValue(); 607 if (counter.size() != 0) { 608 if (orgToVotesString.length() != 0) { 609 orgToVotesString += ", "; 610 } 611 Organization org = entry.getKey(); 612 orgToVotesString += org.toString() + "=" + counter.toString(); 613 } 614 } 615 EnumSet<Organization> conflicted = EnumSet.noneOf(Organization.class); 616 return "{orgToVotes: " + orgToVotesString 617 + ", totals: " + getTotals(conflicted) 618 + ", conflicted: " + conflicted.toString() 619 + "}"; 620 } 621 622 /** 623 * This is now deprecated, since the organization may have multiple votes. 624 * 625 * @param org 626 * @return 627 * @deprecated 628 */ 629 @Deprecated getOrgVote(Organization org)630 public T getOrgVote(Organization org) { 631 return orgToAdd.get(org); 632 } 633 getOrgVoteRaw(Organization orgOfUser)634 public T getOrgVoteRaw(Organization orgOfUser) { 635 return orgToAdd.get(orgOfUser); 636 } 637 getOrgToVotes(Organization org)638 public Map<T, Long> getOrgToVotes(Organization org) { 639 Map<T, Long> result = new LinkedHashMap<>(); 640 MaxCounter<T> counter = orgToVotes.get(org); 641 for (T item : counter) { 642 result.put(item, counter.getCount(item)); 643 } 644 return result; 645 } 646 } 647 648 /** 649 * Static info read from file 650 */ 651 private static Map<Integer, VoterInfo> voterToInfo; 652 653 private static TreeMap<String, Map<Organization, Level>> localeToOrganizationToMaxVote; 654 655 /** 656 * Data built internally 657 */ 658 659 private T winningValue; 660 private T oValue; // optimal value; winning if better approval status than old 661 private T nValue; // next to optimal value 662 private List<T> valuesWithSameVotes = new ArrayList<>(); 663 private Counter<T> totals = null; 664 665 private Status winningStatus; 666 private EnumSet<Organization> conflictedOrganizations = EnumSet 667 .noneOf(Organization.class); 668 private OrganizationToValueAndVote<T> organizationToValueAndVote = new OrganizationToValueAndVote<>(); 669 private T trunkValue; 670 private Status trunkStatus; 671 672 private boolean resolved; 673 private boolean valueIsLocked; 674 private int requiredVotes; 675 private SupplementalDataInfo supplementalDataInfo = SupplementalDataInfo.getInstance(); 676 677 /** 678 * usingKeywordAnnotationVoting: when true, use a special voting method for keyword 679 * annotations that have multiple values separated by bar, like "happy | joyful". 680 * See http://unicode.org/cldr/trac/ticket/10973 . 681 * public, set in STFactory.java; could make it private and add param to 682 * the VoteResolver constructor. 683 */ 684 private boolean usingKeywordAnnotationVoting = false; 685 686 private final Comparator<T> ucaCollator = new Comparator<T>() { 687 Collator col = Collator.getInstance(ULocale.ENGLISH); 688 689 @Override 690 public int compare(T o1, T o2) { 691 return col.compare(String.valueOf(o1), String.valueOf(o2)); 692 } 693 }; 694 695 /** 696 * Set the trunk value and status for this VoteResolver. 697 * 698 * Assume that we don't need to make any changes for INHERITANCE_MARKER here; 699 * the input will have INHERITANCE_MARKER if appropriate; do nothing special 700 * for a specific value that happens to match the Bailey value. 701 * 702 * Reference: https://unicode.org/cldr/trac/ticket/11857 703 * 704 * @param trunkValue the trunk value 705 * @param trunkStatus the trunk status 706 * 707 * TODO: consider renaming: setTrunk to setBaseline; getTrunkValue to getBaselineValue; getTrunkStatus to getBaselineStatus 708 */ setTrunk(T trunkValue, Status trunkStatus)709 public void setTrunk(T trunkValue, Status trunkStatus) { 710 this.trunkValue = trunkValue; 711 this.trunkStatus = trunkValue == null ? Status.missing : trunkStatus; 712 } 713 getTrunkValue()714 public T getTrunkValue() { 715 return trunkValue; 716 } 717 getTrunkStatus()718 public Status getTrunkStatus() { 719 return trunkStatus; 720 } 721 722 /** 723 * You must call this locale whenever you are using a VoteResolver with a new locale. 724 * More efficient to call the CLDRLocale version. 725 * 726 * @param locale 727 * @return 728 * @deprecated need to use the other version to get path-based voting requirements right. 729 */ 730 @Deprecated setLocale(String locale)731 public VoteResolver<T> setLocale(String locale) { 732 setLocale(CLDRLocale.getInstance(locale), null); 733 return this; 734 } 735 736 /** 737 * You must call this locale whenever you are using a VoteResolver with a new locale or a new Pathheader 738 * 739 * @param locale 740 * @return 741 */ setLocale(CLDRLocale locale, PathHeader path)742 public VoteResolver<T> setLocale(CLDRLocale locale, PathHeader path) { 743 requiredVotes = supplementalDataInfo.getRequiredVotes(locale.getLanguageLocale(), path); 744 return this; 745 } 746 747 /** 748 * What are the required votes for this item? 749 * @return the number of votes (as of this writing: usually 4, 8 for established locales) 750 */ getRequiredVotes()751 public int getRequiredVotes() { 752 return requiredVotes; 753 } 754 755 /** 756 * Call this method first, for a new base path. You'll then call add for each value 757 * associated with that base path. 758 */ clear()759 public void clear() { 760 this.trunkValue = null; 761 this.trunkStatus = Status.missing; 762 this.setUsingKeywordAnnotationVoting(false); 763 organizationToValueAndVote.clear(); 764 resolved = valueIsLocked = false; 765 values.clear(); 766 } 767 768 /** 769 * Get the bailey value (what the inherited value would be if there were no 770 * explicit value) for this VoteResolver. 771 * 772 * Throw an exception if !baileySet. 773 * 774 * @return the bailey value. 775 * 776 * Called by STFactory.PerLocaleData.getResolverInternal in the special 777 * circumstance where getWinningValue has returned INHERITANCE_MARKER. 778 */ getBaileyValue()779 public T getBaileyValue() { 780 if (organizationToValueAndVote == null 781 || organizationToValueAndVote.baileySet == false) { 782 throw new IllegalArgumentException("setBaileyValue must be called before getBaileyValue"); 783 } 784 return organizationToValueAndVote.baileyValue; 785 } 786 787 /** 788 * Set the Bailey value (what the inherited value would be if there were no explicit value). 789 * This value is used in handling any {@link CldrUtility.INHERITANCE_MARKER}. 790 * This value must be set <i>before</i> adding values. Usually by calling CLDRFile.getBaileyValue(). 791 */ setBaileyValue(T baileyValue)792 public void setBaileyValue(T baileyValue) { 793 organizationToValueAndVote.baileySet = true; 794 organizationToValueAndVote.baileyValue = baileyValue; 795 } 796 797 /** 798 * Call once for each voter for a value. If there are no voters for an item, then call add(value); 799 * 800 * @param value 801 * @param voter 802 * @param withVotes override to lower the user's voting permission. May be null for default. 803 * @param date 804 * 805 * Called by getResolverInternal 806 */ add(T value, int voter, Integer withVotes, Date date)807 public void add(T value, int voter, Integer withVotes, Date date) { 808 if (resolved) { 809 throw new IllegalArgumentException("Must be called after clear, and before any getters."); 810 } 811 if (withVotes != null && withVotes == Level.LOCKING_VOTES) { 812 valueIsLocked = true; 813 } 814 organizationToValueAndVote.add(value, voter, withVotes, date); 815 values.add(value); 816 } 817 818 /** 819 * Call once for each voter for a value. If there are no voters for an item, then call add(value); 820 * 821 * @param value 822 * @param voter 823 * @param withVotes override to lower the user's voting permission. May be null for default. 824 * 825 * Called only for TestUtilities, not used in Survey Tool. 826 */ add(T value, int voter, Integer withVotes)827 public void add(T value, int voter, Integer withVotes) { 828 if (resolved) { 829 throw new IllegalArgumentException("Must be called after clear, and before any getters."); 830 } 831 Date date = new Date(); 832 organizationToValueAndVote.add(value, voter, withVotes, date); 833 values.add(value); 834 } 835 836 /** 837 * Used only in add(value, voter) for making a pseudo-Date 838 */ 839 private int maxcounter = 100; 840 841 /** 842 * Call once for each voter for a value. If there are no voters for an item, then call add(value); 843 * 844 * @param value 845 * @param voter 846 * 847 * Called by ConsoleCheckCLDR and TestUtilities; not used in SurveyTool. 848 */ add(T value, int voter)849 public void add(T value, int voter) { 850 Date date = new Date(++maxcounter); 851 add(value, voter, null, date); 852 } 853 854 /** 855 * Call if a value has no voters. It is safe to also call this if there is a voter, just unnecessary. 856 * 857 * @param value 858 * @param voter 859 * 860 * Called by getResolverInternal for the baseline (trunk) value; also called for ConsoleCheckCLDR. 861 */ add(T value)862 public void add(T value) { 863 if (resolved) { 864 throw new IllegalArgumentException("Must be called after clear, and before any getters."); 865 } 866 values.add(value); 867 } 868 869 private Set<T> values = new TreeSet<>(ucaCollator); 870 871 private final Comparator<T> votesThenUcaCollator = new Comparator<T>() { 872 Collator col = Collator.getInstance(ULocale.ENGLISH); 873 874 /** 875 * Compare candidate items by vote count, highest vote first. 876 * In the case of ties, favor (a) the baseline (trunk) value, 877 * then (b) votes for inheritance (INHERITANCE_MARKER), 878 * then (c) the alphabetical order (as a last resort). 879 * 880 * Return negative to favor o1, positive to favor o2. 881 */ 882 @Override 883 public int compare(T o1, T o2) { 884 long v1 = organizationToValueAndVote.totalVotes.get(o1); 885 long v2 = organizationToValueAndVote.totalVotes.get(o2); 886 if (v1 != v2) { 887 return v1 < v2 ? 1 : -1; // highest vote first 888 } 889 if (o1.equals(trunkValue)) { 890 return -1; 891 } else if (o2.equals(trunkValue)) { 892 return 1; 893 } 894 if (o1.equals(CldrUtility.INHERITANCE_MARKER)) { 895 return -1; 896 } else if (o2.equals(CldrUtility.INHERITANCE_MARKER)) { 897 return 1; 898 } 899 return col.compare(String.valueOf(o1), String.valueOf(o2)); 900 } 901 }; 902 903 /** 904 * Resolve the votes. Resolution entails counting votes and setting 905 * members for this VoteResolver, including winningStatus, winningValue, 906 * and many others. 907 */ resolveVotes()908 private void resolveVotes() { 909 resolved = true; 910 // get the votes for each organization 911 valuesWithSameVotes.clear(); 912 totals = organizationToValueAndVote.getTotals(conflictedOrganizations); 913 /* Note: getKeysetSortedByCount actually returns a LinkedHashSet, "with predictable iteration order". */ 914 final Set<T> sortedValues = totals.getKeysetSortedByCount(false, votesThenUcaCollator); 915 if (DEBUG) { 916 System.out.println("sortedValues :" + sortedValues.toString()); 917 } 918 919 /* 920 * If there are no (unconflicted) votes, return baseline (trunk) if not null, 921 * else INHERITANCE_MARKER if baileySet, else NO_WINNING_VALUE. 922 * Avoid setting winningValue to null. VoteResolver should be fully in charge of vote resolution. 923 * Note: formerly if trunkValue was null here, winningValue was set to null, such 924 * as for http://localhost:8080/cldr-apps/v#/aa/Numbering_Systems/7b8ee7884f773afa 925 * -- in spite of which the Survey Tool client displayed "latn" (bailey) in the Winning 926 * column. The behavior was originally implemented on the client (JavaScript) and later 927 * (temporarily) as fixWinningValue in DataSection.java. 928 */ 929 if (sortedValues.size() == 0) { 930 if (trunkValue != null) { 931 winningValue = trunkValue; 932 winningStatus = trunkStatus; 933 } else if (organizationToValueAndVote.baileySet) { 934 winningValue = (T) CldrUtility.INHERITANCE_MARKER; 935 winningStatus = Status.missing; 936 } else { 937 /* 938 * TODO: When can this still happen? See https://unicode.org/cldr/trac/ticket/11299 "Example C". 939 * Also http://localhost:8080/cldr-apps/v#/en_CA/Gregorian/ 940 * -- also http://localhost:8080/cldr-apps/v#/aa/Languages_A_D/ 941 * xpath //ldml/localeDisplayNames/languages/language[@type="zh_Hans"][@alt="long"] 942 * See also checkDataRowConsistency in DataSection.java. 943 */ 944 winningValue = (T) NO_WINNING_VALUE; 945 winningStatus = Status.missing; 946 } 947 valuesWithSameVotes.add(winningValue); 948 return; 949 } 950 if (values.size() == 0) { 951 throw new IllegalArgumentException("No values added to resolver"); 952 } 953 954 /* 955 * Copy what is in the the totals field of this VoteResolver for all the 956 * values in sortedValues. This local variable voteCount may be used 957 * subsequently to make adjustments for vote resolution. Those adjustment 958 * may affect the winners in vote resolution, while still preserving the original 959 * voting data including the totals field. 960 */ 961 HashMap<T, Long> voteCount = makeVoteCountMap(sortedValues); 962 963 /* 964 * Adjust sortedValues and voteCount as needed to combine "soft" votes for inheritance 965 * with "hard" votes for the Bailey value. Note that sortedValues and voteCount are 966 * both local variables. 967 */ 968 combineInheritanceWithBaileyForVoting(sortedValues, voteCount); 969 970 /* 971 * Adjust sortedValues and voteCount as needed for annotation keywords. 972 */ 973 if (isUsingKeywordAnnotationVoting()) { 974 adjustAnnotationVoteCounts(sortedValues, voteCount); 975 } 976 977 /* 978 * Perform the actual resolution. 979 */ 980 long weights[] = setBestNextAndSameVoteValues(sortedValues, voteCount); 981 982 oValue = winningValue; 983 984 winningStatus = computeStatus(weights[0], weights[1], trunkStatus); 985 986 // if we are not as good as the trunk, use the trunk 987 if (trunkStatus != null && winningStatus.compareTo(trunkStatus) < 0) { 988 winningStatus = trunkStatus; 989 winningValue = trunkValue; 990 valuesWithSameVotes.clear(); 991 valuesWithSameVotes.add(winningValue); 992 } 993 } 994 995 /** 996 * Make a hash for the vote count of each value in the given sorted list, using 997 * the totals field of this VoteResolver. 998 * 999 * This enables subsequent local adjustment of the effective votes, without change 1000 * to the totals field. Purposes include inheritance and annotation voting. 1001 * 1002 * @param sortedValues the sorted list of values (really a LinkedHashSet, "with predictable iteration order") 1003 * @return the HashMap 1004 */ makeVoteCountMap(Set<T> sortedValues)1005 private HashMap<T, Long> makeVoteCountMap(Set<T> sortedValues) { 1006 HashMap<T, Long> map = new HashMap<>(); 1007 for (T value : sortedValues) { 1008 map.put(value, totals.getCount(value)); 1009 } 1010 return map; 1011 } 1012 1013 /** 1014 * Adjust the given sortedValues and voteCount, if necessary, to combine "hard" and "soft" votes. 1015 * Do nothing unless both hard and soft votes are present. 1016 * 1017 * For voting resolution in which inheritance plays a role, "soft" votes for inheritance 1018 * are distinct from "hard" (explicit) votes for the Bailey value. For resolution, these two kinds 1019 * of votes are treated in combination. If that combination is winning, then the final winner will 1020 * be the hard item or the soft item, whichever has more votes, the soft item winning if they're tied. 1021 * Except for the soft item being favored as a tie-breaker, this function should be symmetrical in its 1022 * handling of hard and soft votes. 1023 * 1024 * Note: now that "↑↑↑" is permitted to participate directly in voting resolution, it becomes significant 1025 * that with Collator.getInstance(ULocale.ENGLISH), "↑↑↑" sorts before "AAA" just as "AAA" sorts before "BBB". 1026 * 1027 * @param sortedValues the set of sorted values, possibly to be modified 1028 * @param voteCount the hash giving the vote count for each value, possibly to be modified 1029 * 1030 * Reference: https://unicode.org/cldr/trac/ticket/11299 1031 */ combineInheritanceWithBaileyForVoting(Set<T> sortedValues, HashMap<T, Long> voteCount)1032 private void combineInheritanceWithBaileyForVoting(Set<T> sortedValues, HashMap<T, Long> voteCount) { 1033 if (organizationToValueAndVote == null 1034 || organizationToValueAndVote.baileySet == false 1035 || organizationToValueAndVote.baileyValue == null) { 1036 return; 1037 } 1038 T hardValue = organizationToValueAndVote.baileyValue; 1039 T softValue = (T) CldrUtility.INHERITANCE_MARKER; 1040 /* 1041 * Check containsKey before get, to avoid NullPointerException. 1042 */ 1043 if (!voteCount.containsKey(hardValue) || !voteCount.containsKey(softValue)) { 1044 return; 1045 } 1046 long hardCount = voteCount.get(hardValue); 1047 long softCount = voteCount.get(softValue); 1048 if (hardCount == 0 || softCount == 0) { 1049 return; 1050 } 1051 T combValue = (hardCount > softCount) ? hardValue : softValue; 1052 T skipValue = (hardCount > softCount) ? softValue : hardValue; 1053 long combinedCount = hardCount + softCount; 1054 voteCount.put(combValue, combinedCount); 1055 voteCount.put(skipValue, 0L); 1056 /* 1057 * Sort again, and omit skipValue 1058 */ 1059 List<T> list = new ArrayList<>(sortedValues); 1060 Collator col = Collator.getInstance(ULocale.ENGLISH); 1061 Collections.sort(list, (v1, v2) -> { 1062 long c1 = (voteCount != null) ? voteCount.get(v1) : totals.getCount(v1); 1063 long c2 = (voteCount != null) ? voteCount.get(v2) : totals.getCount(v2); 1064 if (c1 != c2) { 1065 return (c1 < c2) ? 1 : -1; // decreasing numeric order (most votes wins) 1066 } 1067 return col.compare(String.valueOf(v1), String.valueOf(v2)); 1068 }); 1069 sortedValues.clear(); 1070 for (T value : list) { 1071 if (!value.equals(skipValue)) { 1072 sortedValues.add(value); 1073 } 1074 } 1075 } 1076 1077 /** 1078 * Adjust the effective votes for bar-joined annotations, 1079 * and re-sort the array of values to reflect the adjusted vote counts. 1080 * 1081 * Note: "Annotations provide names and keywords for Unicode characters, currently focusing on emoji." 1082 * For example, an annotation "happy | joyful" has two components "happy" and "joyful". 1083 * References: 1084 * http://unicode.org/cldr/charts/32/annotations/index.html 1085 * http://unicode.org/repos/cldr/trunk/specs/ldml/tr35-general.html#Annotations 1086 * http://unicode.org/repos/cldr/tags/latest/common/annotations/ 1087 * 1088 * This function is where the essential algorithm needs to be implemented 1089 * for http://unicode.org/cldr/trac/ticket/10973 1090 * 1091 * @param sortedValues the set of sorted values 1092 * @param voteCount the hash giving the vote count for each value in sortedValues 1093 * 1094 * public for unit testing, see TestAnnotationVotes.java 1095 */ adjustAnnotationVoteCounts(Set<T> sortedValues, HashMap<T, Long> voteCount)1096 public void adjustAnnotationVoteCounts(Set<T> sortedValues, HashMap<T, Long> voteCount) { 1097 if (voteCount == null || sortedValues == null) { 1098 return; 1099 } 1100 // Make compMap map individual components to cumulative vote counts. 1101 HashMap<T, Long> compMap = makeAnnotationComponentMap(sortedValues, voteCount); 1102 1103 // Save a copy of the "raw" vote count before adjustment, since it's needed by promoteSuperiorAnnotationSuperset. 1104 HashMap<T, Long> rawVoteCount = new HashMap<>(voteCount); 1105 1106 // Calculate new counts for original values, based on components. 1107 calculateNewCountsBasedOnAnnotationComponents(sortedValues, voteCount, compMap); 1108 1109 // Re-sort sortedValues based on voteCount. 1110 resortValuesBasedOnAdjustedVoteCounts(sortedValues, voteCount); 1111 1112 // If the set that so far is winning has supersets with superior raw vote count, promote the supersets. 1113 promoteSuperiorAnnotationSuperset(sortedValues, voteCount, rawVoteCount); 1114 } 1115 1116 /** 1117 * Make a hash that maps individual annotation components to cumulative vote counts. 1118 * 1119 * For example, 3 votes for "a|b" and 2 votes for "a|c" makes 5 votes for "a", 3 for "b", and 2 for "c". 1120 * 1121 * @param sortedValues the set of sorted values 1122 * @param voteCount the hash giving the vote count for each value in sortedValues 1123 */ makeAnnotationComponentMap(Set<T> sortedValues, HashMap<T, Long> voteCount)1124 private HashMap<T, Long> makeAnnotationComponentMap(Set<T> sortedValues, HashMap<T, Long> voteCount) { 1125 HashMap<T, Long> compMap = new HashMap<>(); 1126 for (T value : sortedValues) { 1127 Long count = voteCount.get(value); 1128 List<T> comps = splitAnnotationIntoComponentsList(value); 1129 for (T comp : comps) { 1130 if (compMap.containsKey(comp)) { 1131 compMap.replace(comp, compMap.get(comp) + count); 1132 } 1133 else { 1134 compMap.put(comp, count); 1135 } 1136 } 1137 } 1138 if (DEBUG) { 1139 System.out.println("\n\tComponents in adjustAnnotationVoteCounts:"); 1140 for (T comp : compMap.keySet()) { 1141 System.out.println("\t" + comp + ":" + compMap.get(comp)); 1142 } 1143 } 1144 return compMap; 1145 } 1146 1147 /** 1148 * Calculate new counts for original values, based on annotation components. 1149 * 1150 * Find the total votes for each component (e.g., "b" in "b|c"). As the "modified" 1151 * vote for the set, use the geometric mean of the components in the set. 1152 * 1153 * Order the sets by that mean value, then by the smallest number of items in 1154 * the set, then the fallback we always use (alphabetical). 1155 * 1156 * @param sortedValues the set of sorted values 1157 * @param voteCount the hash giving the vote count for each value in sortedValues 1158 * @param compMap the hash that maps individual components to cumulative vote counts 1159 * 1160 * See http://unicode.org/cldr/trac/ticket/10973 1161 */ calculateNewCountsBasedOnAnnotationComponents(Set<T> sortedValues, HashMap<T, Long> voteCount, HashMap<T, Long> compMap)1162 private void calculateNewCountsBasedOnAnnotationComponents(Set<T> sortedValues, HashMap<T, Long> voteCount, HashMap<T, Long> compMap) { 1163 voteCount.clear(); 1164 for (T value : sortedValues) { 1165 List<T> comps = splitAnnotationIntoComponentsList(value); 1166 double product = 1.0; 1167 for (T comp : comps) { 1168 product *= compMap.get(comp); 1169 } 1170 /* Rounding to long integer here loses precision. We tried multiplying by ten before rounding, 1171 * to reduce problems with different doubles getting rounded to identical longs, but that had 1172 * unfortunate side-effects involving thresholds (see getRequiredVotes). An eventual improvement 1173 * may be to use doubles or floats for all vote counts. 1174 */ 1175 Long newCount = Math.round(Math.pow(product, 1.0 / comps.size())); // geometric mean 1176 voteCount.put(value, newCount); 1177 } 1178 } 1179 1180 /** 1181 * Split an annotation into a list of components. 1182 * 1183 * For example, split "happy | joyful" into ["happy", "joyful"]. 1184 * 1185 * @param value the value like "happy | joyful" 1186 * @return the list like ["happy", "joyful"] 1187 * 1188 * Called by makeAnnotationComponentMap and calculateNewCountsBasedOnAnnotationComponents. 1189 * Short, but needs encapsulation, should be consistent with similar code in DisplayAndInputProcessor.java. 1190 */ splitAnnotationIntoComponentsList(T value)1191 private List<T> splitAnnotationIntoComponentsList(T value) { 1192 return (List<T>) DisplayAndInputProcessor.SPLIT_BAR.splitToList((CharSequence) value); 1193 } 1194 1195 /** 1196 * Re-sort the set of values to match the adjusted vote counts based on annotation components. 1197 * 1198 * Resolve ties using ULocale.ENGLISH collation for consistency with votesThenUcaCollator. 1199 * 1200 * @param sortedValues the set of sorted values, maybe no longer sorted the way we want 1201 * @param voteCount the hash giving the adjusted vote count for each value in sortedValues 1202 */ resortValuesBasedOnAdjustedVoteCounts(Set<T> sortedValues, HashMap<T, Long> voteCount)1203 private void resortValuesBasedOnAdjustedVoteCounts(Set<T> sortedValues, HashMap<T, Long> voteCount) { 1204 List<T> list = new ArrayList<>(sortedValues); 1205 Collator col = Collator.getInstance(ULocale.ENGLISH); 1206 Collections.sort(list, (v1, v2) -> { 1207 long c1 = voteCount.get(v1), c2 = voteCount.get(v2); 1208 if (c1 != c2) { 1209 return (c1 < c2) ? 1 : -1; // decreasing numeric order (most votes wins) 1210 } 1211 int size1 = splitAnnotationIntoComponentsList(v1).size(); 1212 int size2 = splitAnnotationIntoComponentsList(v2).size(); 1213 if (size1 != size2) { 1214 return (size1 < size2) ? -1 : 1; // increasing order of size (smallest set wins) 1215 } 1216 return col.compare(String.valueOf(v1), String.valueOf(v2)); 1217 }); 1218 sortedValues.clear(); 1219 for (T value : list) { 1220 sortedValues.add(value); 1221 } 1222 } 1223 1224 /** 1225 * For annotation votes, if the set that so far is winning has one or more supersets with "superior" (see 1226 * below) raw vote count, promote those supersets to become the new winner, and also the new second place 1227 * if there are two or more superior supersets. 1228 * 1229 * That is, after finding the set X with the largest geometric mean, check whether there are any supersets 1230 * with "superior" raw votes, and that don't exceed the width limit. If so, promote Y, the one of those 1231 * supersets with the most raw votes (using the normal tie breaker), to be the winning set. 1232 * 1233 * "Superior" here means that rawVote(Y) ≥ rawVote(X) + 2, where the value 2 (see requiredGap) is for the 1234 * purpose of requiring at least one non-guest vote. 1235 * 1236 * If any other "superior" supersets exist, promote to second place the one with the next most raw votes. 1237 * 1238 * Accomplish promotion by increasing vote counts in the voteCount hash. 1239 * 1240 * @param sortedValues the set of sorted values 1241 * @param voteCount the vote count for each value in sortedValues AFTER calculateNewCountsBasedOnAnnotationComponents; 1242 * it gets modified if superior subsets exist 1243 * @param rawVoteCount the vote count for each value in sortedValues BEFORE calculateNewCountsBasedOnAnnotationComponents; 1244 * rawVoteCount is not changed by this function 1245 * 1246 * Reference: https://unicode.org/cldr/trac/ticket/10973 1247 */ promoteSuperiorAnnotationSuperset(Set<T> sortedValues, HashMap<T, Long> voteCount, HashMap<T, Long> rawVoteCount)1248 private void promoteSuperiorAnnotationSuperset(Set<T> sortedValues, HashMap<T, Long> voteCount, HashMap<T, Long> rawVoteCount) { 1249 final long requiredGap = 2; 1250 T oldWinner = null; 1251 long oldWinnerRawCount = 0; 1252 LinkedHashSet<T> oldWinnerComps = null; 1253 LinkedHashSet<T> superiorSupersets = null; 1254 for (T value : sortedValues) { 1255 if (oldWinner == null) { 1256 oldWinner = value; 1257 oldWinnerRawCount = rawVoteCount.get(value); 1258 oldWinnerComps = new LinkedHashSet<>(splitAnnotationIntoComponentsList(value)); 1259 } else { 1260 Set<T> comps = new LinkedHashSet<>(splitAnnotationIntoComponentsList(value)); 1261 if (comps.size() <= CheckWidths.MAX_COMPONENTS_PER_ANNOTATION && 1262 comps.containsAll(oldWinnerComps) && 1263 rawVoteCount.get(value) >= oldWinnerRawCount + requiredGap) { 1264 if (superiorSupersets == null) { 1265 superiorSupersets = new LinkedHashSet<>(); 1266 } 1267 superiorSupersets.add(value); 1268 } 1269 } 1270 } 1271 if (superiorSupersets != null) { 1272 // Sort the supersets by raw vote count, then make their adjusted vote counts higher than the old winner's. 1273 resortValuesBasedOnAdjustedVoteCounts(superiorSupersets, rawVoteCount); 1274 T newWinner = null, newSecond = null; // only adjust votes for first and second place 1275 for (T value : superiorSupersets) { 1276 if (newWinner == null) { 1277 newWinner = value; 1278 voteCount.put(newWinner, voteCount.get(oldWinner) + 2); // more than oldWinner and newSecond 1279 } else if (newSecond == null) { 1280 newSecond = value; 1281 voteCount.put(newSecond, voteCount.get(oldWinner) + 1); // more than oldWinner, less than newWinner 1282 break; 1283 } 1284 } 1285 resortValuesBasedOnAdjustedVoteCounts(sortedValues, voteCount); 1286 } 1287 } 1288 1289 /** 1290 * Given a nonempty list of sorted values, and a hash with their vote counts, set these members 1291 * of this VoteResolver: 1292 * winningValue, nValue, valuesWithSameVotes (which is empty when this function is called). 1293 * 1294 * @param sortedValues the set of sorted values 1295 * @param voteCount the hash giving the vote count for each value 1296 * @return an array of two longs, the weights for the best and next-best values. 1297 */ setBestNextAndSameVoteValues(Set<T> sortedValues, HashMap<T, Long> voteCount)1298 private long[] setBestNextAndSameVoteValues(Set<T> sortedValues, HashMap<T, Long> voteCount) { 1299 1300 long weightArray[] = new long[2]; 1301 weightArray[0] = 0; 1302 weightArray[1] = 0; 1303 nValue = null; 1304 1305 /* 1306 * Loop through the sorted values, at least the first (best) for winningValue, 1307 * and the second (if any) for nValue (else nValue stays null), 1308 * and subsequent values that have as many votes as the first, 1309 * to add to valuesWithSameVotes. 1310 */ 1311 int i = -1; 1312 Iterator<T> iterator = sortedValues.iterator(); 1313 for (T value : sortedValues) { 1314 ++i; 1315 long valueWeight = voteCount.get(value); 1316 if (i == 0) { 1317 winningValue = value; 1318 weightArray[0] = valueWeight; 1319 valuesWithSameVotes.add(value); 1320 } else { 1321 if (i == 1) { 1322 // get the next item if there is one 1323 if (iterator.hasNext()) { 1324 nValue = value; 1325 weightArray[1] = valueWeight; 1326 } 1327 } 1328 if (valueWeight == weightArray[0]) { 1329 valuesWithSameVotes.add(value); 1330 } else { 1331 break; 1332 } 1333 } 1334 } 1335 return weightArray; 1336 } 1337 1338 /** 1339 * Compute the status for the winning value. 1340 * 1341 * @param weight1 the weight (vote count) for the best value 1342 * @param weight2 the weight (vote count) for the next-best value 1343 * @param oldStatus the old status (trunkStatus) 1344 * @return the Status 1345 */ computeStatus(long weight1, long weight2, Status oldStatus)1346 private Status computeStatus(long weight1, long weight2, Status oldStatus) { 1347 if (weight1 > weight2 && weight1 >= requiredVotes) { 1348 return Status.approved; 1349 } 1350 if (weight1 > weight2 && 1351 (weight1 >= 4 && Status.contributed.compareTo(oldStatus) > 0 1352 || weight1 >= 2 && organizationToValueAndVote.getOrgCount(winningValue) >= 2) ) { 1353 return Status.contributed; 1354 } 1355 if (weight1 >= weight2 && weight1 >= 2) { 1356 return Status.provisional; 1357 } 1358 return Status.unconfirmed; 1359 } 1360 getPossibleWinningStatus()1361 public Status getPossibleWinningStatus() { 1362 if (!resolved) { 1363 resolveVotes(); 1364 } 1365 Status possibleStatus = computeStatus(organizationToValueAndVote.getBestPossibleVote(), 0, trunkStatus); 1366 return possibleStatus.compareTo(winningStatus) > 0 ? possibleStatus : winningStatus; 1367 } 1368 1369 /** 1370 * If the winning item is not approved, and if all the people who voted had voted for the winning item, 1371 * would it have made contributed or approved? 1372 * 1373 * @return 1374 */ isDisputed()1375 public boolean isDisputed() { 1376 if (!resolved) { 1377 resolveVotes(); 1378 } 1379 if (winningStatus.compareTo(VoteResolver.Status.contributed) >= 0) { 1380 return false; 1381 } 1382 VoteResolver.Status possibleStatus = getPossibleWinningStatus(); 1383 if (possibleStatus.compareTo(VoteResolver.Status.contributed) >= 0) { 1384 return true; 1385 } 1386 return false; 1387 } 1388 getWinningStatus()1389 public Status getWinningStatus() { 1390 if (!resolved) { 1391 resolveVotes(); 1392 } 1393 return winningStatus; 1394 } 1395 1396 /** 1397 * Returns O Value as described in http://cldr.unicode.org/index/process#TOC-Voting-Process. 1398 * Not always the same as the Winning Value. 1399 * 1400 * @return 1401 */ getOValue()1402 public T getOValue() { 1403 if (!resolved) { 1404 resolveVotes(); 1405 } 1406 return oValue; 1407 } 1408 1409 /** 1410 * Returns N Value as described in http://cldr.unicode.org/index/process#TOC-Voting-Process. 1411 * Not always the same as the Winning Value. 1412 * 1413 * @return 1414 */ getNValue()1415 public T getNValue() { 1416 if (!resolved) { 1417 resolveVotes(); 1418 } 1419 return nValue; 1420 } 1421 1422 /** 1423 * @deprecated 1424 */ 1425 @Deprecated getNextToWinningValue()1426 public T getNextToWinningValue() { 1427 return getNValue(); 1428 } 1429 1430 /** 1431 * Returns Winning Value as described in http://cldr.unicode.org/index/process#TOC-Voting-Process. 1432 * Not always the same as the O Value. 1433 * 1434 * @return 1435 */ getWinningValue()1436 public T getWinningValue() { 1437 if (!resolved) { 1438 resolveVotes(); 1439 } 1440 return winningValue; 1441 } 1442 getValuesWithSameVotes()1443 public List<T> getValuesWithSameVotes() { 1444 if (!resolved) { 1445 resolveVotes(); 1446 } 1447 return new ArrayList<>(valuesWithSameVotes); 1448 } 1449 getConflictedOrganizations()1450 public EnumSet<Organization> getConflictedOrganizations() { 1451 if (!resolved) { 1452 resolveVotes(); 1453 } 1454 return conflictedOrganizations; 1455 } 1456 1457 /** 1458 * What value did this organization vote for? 1459 * 1460 * @param org 1461 * @return 1462 */ getOrgVote(Organization org)1463 public T getOrgVote(Organization org) { 1464 return organizationToValueAndVote.getOrgVote(org); 1465 } 1466 getOrgToVotes(Organization org)1467 public Map<T, Long> getOrgToVotes(Organization org) { 1468 return organizationToValueAndVote.getOrgToVotes(org); 1469 } 1470 getNameTime()1471 public Map<String, Long> getNameTime() { 1472 return organizationToValueAndVote.getNameTime(); 1473 } 1474 1475 /** 1476 * Get a String representation of this VoteResolver. 1477 * This is sent to the client as "voteResolver.raw" and is used only for debugging. 1478 * 1479 * Compare SurveyAjax.JSONWriter.wrap(VoteResolver<String>) which creates the data 1480 * actually used by the client. 1481 */ 1482 @Override toString()1483 public String toString() { 1484 return "{" 1485 + "bailey: " + (organizationToValueAndVote.baileySet ? ("“" + organizationToValueAndVote.baileyValue + "” ") : "none ") 1486 + "trunk: {" + trunkValue + ", " + trunkStatus + "}, " 1487 + organizationToValueAndVote 1488 + ", sameVotes: " + valuesWithSameVotes 1489 + ", O: " + getOValue() 1490 + ", N: " + getNValue() 1491 + ", totals: " + totals 1492 + ", winning: {" + getWinningValue() + ", " + getWinningStatus() + "}" 1493 + "}"; 1494 } 1495 getVoterToInfo()1496 private static Map<Integer, VoterInfo> getVoterToInfo() { 1497 synchronized (VoteResolver.class) { 1498 return voterToInfo; 1499 } 1500 } 1501 getInfoForVoter(int voter)1502 public static VoterInfo getInfoForVoter(int voter) { 1503 return getVoterToInfo().get(voter); 1504 } 1505 1506 /** 1507 * Set the voter info. 1508 * <p> 1509 * Synchronized, however, once this is called, you must NOT change the contents of your copy of testVoterToInfo. You 1510 * can create a whole new one and set it. 1511 */ setVoterToInfo(Map<Integer, VoterInfo> testVoterToInfo)1512 public static void setVoterToInfo(Map<Integer, VoterInfo> testVoterToInfo) { 1513 synchronized (VoteResolver.class) { 1514 VoteResolver.voterToInfo = testVoterToInfo; 1515 } 1516 if (DEBUG) { 1517 for (int id : testVoterToInfo.keySet()) { 1518 System.out.println("\t" + id + "=" + testVoterToInfo.get(id)); 1519 } 1520 } 1521 computeMaxVotes(); 1522 } 1523 1524 /** 1525 * Set the voter info from a users.xml file. 1526 * <p> 1527 * Synchronized, however, once this is called, you must NOT change the contents of your copy of testVoterToInfo. You 1528 * can create a whole new one and set it. 1529 */ setVoterToInfo(String fileName)1530 public static void setVoterToInfo(String fileName) { 1531 MyHandler myHandler = new MyHandler(); 1532 XMLFileReader xfr = new XMLFileReader().setHandler(myHandler); 1533 xfr.read(fileName, XMLFileReader.CONTENT_HANDLER | XMLFileReader.ERROR_HANDLER, false); 1534 setVoterToInfo(myHandler.testVoterToInfo); 1535 1536 computeMaxVotes(); 1537 } 1538 computeMaxVotes()1539 private static synchronized void computeMaxVotes() { 1540 // compute the localeToOrganizationToMaxVote 1541 localeToOrganizationToMaxVote = new TreeMap<>(); 1542 for (int voter : getVoterToInfo().keySet()) { 1543 VoterInfo info = getVoterToInfo().get(voter); 1544 if (info.getLevel() == Level.tc || info.getLevel() == Level.locked) { 1545 continue; // skip TCs, locked 1546 } 1547 1548 for (String locale : info.getLocales()) { 1549 Map<Organization, Level> organizationToMaxVote = localeToOrganizationToMaxVote.get(locale); 1550 if (organizationToMaxVote == null) { 1551 localeToOrganizationToMaxVote.put(locale, 1552 organizationToMaxVote = new TreeMap<>()); 1553 } 1554 Level maxVote = organizationToMaxVote.get(info.getOrganization()); 1555 if (maxVote == null || info.getLevel().compareTo(maxVote) > 0) { 1556 organizationToMaxVote.put(info.getOrganization(), info.getLevel()); 1557 // System.out.println("Example best voter for " + locale + " for " + info.organization + " is " + 1558 // info); 1559 } 1560 } 1561 } 1562 CldrUtility.protectCollection(localeToOrganizationToMaxVote); 1563 } 1564 1565 /** 1566 * Handles fine in xml format, turning into: 1567 * //users[@host="sarasvati.unicode.org"]/user[@id="286"][@email="mike.tardif@adobe.com"]/level[@n="1"][@type="TC"] 1568 * //users[@host="sarasvati.unicode.org"]/user[@id="286"][@email="mike.tardif@adobe.com"]/name 1569 * Mike Tardif 1570 * //users[@host="sarasvati.unicode.org"]/user[@id="286"][@email="mike.tardif@adobe.com"]/org 1571 * Adobe 1572 * //users[@host="sarasvati.unicode.org"]/user[@id="286"][@email="mike.tardif@adobe.com"]/locales[@type="edit"] 1573 * 1574 * Steven's new format: 1575 * //users[@generated="Wed May 07 15:57:15 PDT 2008"][@host="tintin"][@obscured="true"] 1576 * /user[@id="286"][@email="?@??.??"] 1577 * /level[@n="1"][@type="TC"] 1578 */ 1579 1580 static class MyHandler extends XMLFileReader.SimpleHandler { 1581 private static final Pattern userPathMatcher = Pattern 1582 .compile( 1583 "//users(?:[^/]*)" 1584 + "/user\\[@id=\"([^\"]*)\"](?:[^/]*)" 1585 + "/(" 1586 + "org" + 1587 "|name" + 1588 "|level\\[@n=\"([^\"]*)\"]\\[@type=\"([^\"]*)\"]" + 1589 "|locales\\[@type=\"([^\"]*)\"]" + 1590 "(?:/locale\\[@id=\"([^\"]*)\"])?" 1591 + ")", 1592 Pattern.COMMENTS); 1593 1594 enum Group { 1595 all, userId, mainType, n, levelType, localeType, localeId; get(Matcher matcher)1596 String get(Matcher matcher) { 1597 return matcher.group(this.ordinal()); 1598 } 1599 } 1600 1601 private static final boolean DEBUG_HANDLER = false; 1602 Map<Integer, VoterInfo> testVoterToInfo = new TreeMap<>(); 1603 Matcher matcher = userPathMatcher.matcher(""); 1604 1605 @Override handlePathValue(String path, String value)1606 public void handlePathValue(String path, String value) { 1607 if (DEBUG_HANDLER) 1608 System.out.println(path + "\t" + value); 1609 if (matcher.reset(path).matches()) { 1610 if (DEBUG_HANDLER) { 1611 for (int i = 1; i <= matcher.groupCount(); ++i) { 1612 Group group = Group.values()[i]; 1613 System.out.println(i + "\t" + group + "\t" + group.get(matcher)); 1614 } 1615 } 1616 int id = Integer.parseInt(Group.userId.get(matcher)); 1617 VoterInfo voterInfo = testVoterToInfo.get(id); 1618 if (voterInfo == null) { 1619 testVoterToInfo.put(id, voterInfo = new VoterInfo()); 1620 } 1621 final String mainType = Group.mainType.get(matcher); 1622 if (mainType.equals("org")) { 1623 Organization org = Organization.fromString(value); 1624 voterInfo.setOrganization(org); 1625 value = org.name(); // copy name back into value 1626 } else if (mainType.equals("name")) { 1627 voterInfo.setName(value); 1628 } else if (mainType.startsWith("level")) { 1629 String level = Group.levelType.get(matcher).toLowerCase(); 1630 voterInfo.setLevel(Level.valueOf(level)); 1631 } else if (mainType.startsWith("locale")) { 1632 final String localeIdString = Group.localeId.get(matcher); 1633 if (localeIdString != null) { 1634 voterInfo.addLocale(localeIdString.split("_")[0]); 1635 } else if (DEBUG_HANDLER) { 1636 System.out.println("\tskipping"); 1637 } 1638 } else if (DEBUG_HANDLER) { 1639 System.out.println("\tFailed match* with " + path + "=" + value); 1640 } 1641 } else { 1642 System.out.println("\tFailed match with " + path + "=" + value); 1643 } 1644 } 1645 } 1646 getIdToPath(String fileName)1647 public static Map<Integer, String> getIdToPath(String fileName) { 1648 XPathTableHandler myHandler = new XPathTableHandler(); 1649 XMLFileReader xfr = new XMLFileReader().setHandler(myHandler); 1650 xfr.read(fileName, XMLFileReader.CONTENT_HANDLER | XMLFileReader.ERROR_HANDLER, false); 1651 return myHandler.pathIdToPath; 1652 } 1653 1654 static class XPathTableHandler extends XMLFileReader.SimpleHandler { 1655 Matcher matcher = Pattern.compile("id=\"([0-9]+)\"").matcher(""); 1656 Map<Integer, String> pathIdToPath = new HashMap<>(); 1657 1658 @Override handlePathValue(String path, String value)1659 public void handlePathValue(String path, String value) { 1660 // <xpathTable host="tintin.local" date="Tue Apr 29 14:34:32 PDT 2008" count="18266" > 1661 // <xpath 1662 // id="1">//ldml/dates/calendars/calendar[@type="gregorian"]/dateFormats/dateFormatLength[@type="short"]/dateFormat[@type="standard"]/pattern[@type="standard"]</xpath> 1663 if (!matcher.reset(path).find()) { 1664 throw new IllegalArgumentException("Unknown path " + path); 1665 } 1666 pathIdToPath.put(Integer.parseInt(matcher.group(1)), value); 1667 } 1668 } 1669 getBaseToAlternateToInfo(String fileName)1670 public static Map<Integer, Map<Integer, CandidateInfo>> getBaseToAlternateToInfo(String fileName) { 1671 try { 1672 VotesHandler myHandler = new VotesHandler(); 1673 XMLFileReader xfr = new XMLFileReader().setHandler(myHandler); 1674 xfr.read(fileName, XMLFileReader.CONTENT_HANDLER | XMLFileReader.ERROR_HANDLER, false); 1675 return myHandler.basepathToInfo; 1676 } catch (Exception e) { 1677 throw (RuntimeException) new IllegalArgumentException("Can't handle file: " + fileName).initCause(e); 1678 } 1679 } 1680 1681 public enum Type { 1682 proposal, optimal 1683 } 1684 1685 public static class CandidateInfo { 1686 public Status oldStatus; 1687 public Type surveyType; 1688 public Status surveyStatus; 1689 public Set<Integer> voters = new TreeSet<>(); 1690 1691 @Override toString()1692 public String toString() { 1693 StringBuilder voterString = new StringBuilder("{"); 1694 for (int voter : voters) { 1695 VoterInfo voterInfo = getInfoForVoter(voter); 1696 if (voterString.length() > 1) { 1697 voterString.append(" "); 1698 } 1699 voterString.append(voter); 1700 if (voterInfo != null) { 1701 voterString.append(" ").append(voterInfo); 1702 } 1703 } 1704 voterString.append("}"); 1705 return "{oldStatus: " + oldStatus 1706 + ", surveyType: " + surveyType 1707 + ", surveyStatus: " + surveyStatus 1708 + ", voters: " + voterString 1709 + "};"; 1710 } 1711 } 1712 1713 /* 1714 * <locale-votes host="tintin.local" date="Tue Apr 29 14:34:32 PDT 2008" 1715 * oldVersion="1.5.1" currentVersion="1.6" resolved="false" locale="zu"> 1716 * <row baseXpath="1"> 1717 * <item xpath="2855" type="proposal" id="1" status="unconfirmed"> 1718 * <old status="unconfirmed"/> 1719 * </item> 1720 * <item xpath="1" type="optimal" id="56810" status="confirmed"> 1721 * <vote user="210"/> 1722 * </item> 1723 * </row> 1724 * ... 1725 * A base path has a set of candidates. Each candidate has various items of information. 1726 */ 1727 static class VotesHandler extends XMLFileReader.SimpleHandler { 1728 Map<Integer, Map<Integer, CandidateInfo>> basepathToInfo = new TreeMap<>(); 1729 1730 @Override handlePathValue(String path, String value)1731 public void handlePathValue(String path, String value) { 1732 try { 1733 XPathParts parts = XPathParts.getFrozenInstance(path); 1734 if (parts.size() < 2) { 1735 // empty data 1736 return; 1737 } 1738 int baseId = Integer.parseInt(parts.getAttributeValue(1, "baseXpath")); 1739 Map<Integer, CandidateInfo> info = basepathToInfo.get(baseId); 1740 if (info == null) { 1741 basepathToInfo.put(baseId, info = new TreeMap<>()); 1742 } 1743 int itemId = Integer.parseInt(parts.getAttributeValue(2, "xpath")); 1744 CandidateInfo candidateInfo = info.get(itemId); 1745 if (candidateInfo == null) { 1746 info.put(itemId, candidateInfo = new CandidateInfo()); 1747 candidateInfo.surveyType = Type.valueOf(parts.getAttributeValue(2, "type")); 1748 candidateInfo.surveyStatus = Status.valueOf(fixBogusDraftStatusValues(parts.getAttributeValue(2, 1749 "status"))); 1750 // ignore id 1751 } 1752 if (parts.size() < 4) { 1753 return; 1754 } 1755 final String lastElement = parts.getElement(3); 1756 if (lastElement.equals("old")) { 1757 candidateInfo.oldStatus = Status.valueOf(fixBogusDraftStatusValues(parts.getAttributeValue(3, 1758 "status"))); 1759 } else if (lastElement.equals("vote")) { 1760 candidateInfo.voters.add(Integer.parseInt(parts.getAttributeValue(3, "user"))); 1761 } else { 1762 throw new IllegalArgumentException("unknown option: " + path); 1763 } 1764 } catch (Exception e) { 1765 throw (RuntimeException) new IllegalArgumentException("Can't handle path: " + path).initCause(e); 1766 } 1767 } 1768 1769 } 1770 getOrganizationToMaxVote(String locale)1771 public static Map<Organization, Level> getOrganizationToMaxVote(String locale) { 1772 locale = locale.split("_")[0]; // take base language 1773 Map<Organization, Level> result = localeToOrganizationToMaxVote.get(locale); 1774 if (result == null) { 1775 result = Collections.emptyMap(); 1776 } 1777 return result; 1778 } 1779 getOrganizationToMaxVote(Set<Integer> voters)1780 public static Map<Organization, Level> getOrganizationToMaxVote(Set<Integer> voters) { 1781 Map<Organization, Level> orgToMaxVoteHere = new TreeMap<>(); 1782 for (int voter : voters) { 1783 VoterInfo info = getInfoForVoter(voter); 1784 if (info == null) { 1785 continue; // skip unknown voter 1786 } 1787 Level maxVote = orgToMaxVoteHere.get(info.getOrganization()); 1788 if (maxVote == null || info.getLevel().compareTo(maxVote) > 0) { 1789 orgToMaxVoteHere.put(info.getOrganization(), info.getLevel()); 1790 // System.out.println("*Best voter for " + info.organization + " is " + info); 1791 } 1792 } 1793 return orgToMaxVoteHere; 1794 } 1795 1796 public static class UnknownVoterException extends RuntimeException { 1797 /** 1798 * 1799 */ 1800 private static final long serialVersionUID = 3430877787936678609L; 1801 int voter; 1802 UnknownVoterException(int voter)1803 public UnknownVoterException(int voter) { 1804 this.voter = voter; 1805 } 1806 1807 @Override toString()1808 public String toString() { 1809 return "Unknown voter: " + voter; 1810 } 1811 getVoter()1812 public int getVoter() { 1813 return voter; 1814 } 1815 } 1816 fixBogusDraftStatusValues(String attributeValue)1817 public static String fixBogusDraftStatusValues(String attributeValue) { 1818 if (attributeValue == null) return "approved"; 1819 if ("confirmed".equals(attributeValue)) return "approved"; 1820 if ("true".equals(attributeValue)) return "unconfirmed"; 1821 if ("unknown".equals(attributeValue)) return "unconfirmed"; 1822 return attributeValue; 1823 } 1824 size()1825 public int size() { 1826 return values.size(); 1827 } 1828 1829 /** 1830 * Returns a map from value to resolved vote count, in descending order. 1831 * If the winning item is not there, insert at the front. 1832 * If the baseline (trunk) item is not there, insert at the end. 1833 * 1834 * @return the map 1835 */ getResolvedVoteCounts()1836 public Map<T, Long> getResolvedVoteCounts() { 1837 if (!resolved) { 1838 resolveVotes(); 1839 } 1840 Map<T, Long> result = new LinkedHashMap<>(); 1841 if (winningValue != null && !totals.containsKey(winningValue)) { 1842 result.put(winningValue, 0L); 1843 } 1844 for (T value : totals.getKeysetSortedByCount(false, votesThenUcaCollator)) { 1845 result.put(value, totals.get(value)); 1846 } 1847 if (trunkValue != null && !totals.containsKey(trunkValue)) { 1848 result.put(trunkValue, 0L); 1849 } 1850 for (T value : organizationToValueAndVote.totalVotes.getMap().keySet()) { 1851 if (!result.containsKey(value)) { 1852 result.put(value, 0L); 1853 } 1854 } 1855 if (DEBUG) { 1856 System.out.println("getResolvedVoteCounts :" + result.toString()); 1857 } 1858 return result; 1859 } 1860 getStatusForOrganization(Organization orgOfUser)1861 public VoteStatus getStatusForOrganization(Organization orgOfUser) { 1862 if (!resolved) { 1863 resolveVotes(); 1864 } 1865 1866 T win = getWinningValue(); 1867 T orgVote = organizationToValueAndVote.getOrgVoteRaw(orgOfUser); 1868 1869 if (!equalsOrgVote(win, orgVote)) { 1870 // We voted and lost 1871 return VoteStatus.losing; 1872 } 1873 1874 Status winStatus = getWinningStatus(); 1875 boolean provisionalOrWorse = Status.provisional.compareTo(winStatus) >= 0; 1876 1877 // get the number of other values with votes. 1878 int itemsWithVotes = organizationToValueAndVote.countValuesWithVotes(); 1879 T singleVotedItem = organizationToValueAndVote.getSingleVotedItem(); 1880 1881 if (itemsWithVotes > 1) { 1882 // If there are votes for two items, we should look at them. 1883 return VoteStatus.disputed; 1884 } else if (!equalsOrgVote(win, singleVotedItem)) { // singleVotedItem != null && ... 1885 // If someone voted but didn't win 1886 return VoteStatus.disputed; 1887 } else if (provisionalOrWorse) { 1888 // If the value is provisional, it needs more votes. 1889 return VoteStatus.provisionalOrWorse; 1890 } else if (itemsWithVotes == 0) { 1891 // The value is ok, but we capture that there are no votes, for revealing items like unsync'ed 1892 return VoteStatus.ok_novotes; 1893 } else { 1894 // We voted, we won, value is approved, no disputes, have votes 1895 return VoteStatus.ok; 1896 } 1897 } 1898 equalsOrgVote(T value, T orgVote)1899 private boolean equalsOrgVote(T value, T orgVote) { 1900 return orgVote == null 1901 || orgVote.equals(value) 1902 || CldrUtility.INHERITANCE_MARKER.equals(value) 1903 && orgVote.equals(organizationToValueAndVote.baileyValue); 1904 } 1905 1906 /** 1907 * Is this VoteResolver using keyword annotation voting? 1908 * 1909 * @return true or false 1910 */ isUsingKeywordAnnotationVoting()1911 public boolean isUsingKeywordAnnotationVoting() { 1912 return usingKeywordAnnotationVoting; 1913 } 1914 1915 /** 1916 * Set whether this VoteResolver should use keyword annotation voting. 1917 * 1918 * @param usingKeywordAnnotationVoting true or false 1919 */ setUsingKeywordAnnotationVoting(boolean usingKeywordAnnotationVoting)1920 public void setUsingKeywordAnnotationVoting(boolean usingKeywordAnnotationVoting) { 1921 this.usingKeywordAnnotationVoting = usingKeywordAnnotationVoting; 1922 } 1923 1924 /** 1925 * Is the value locked for this locale+path? 1926 * 1927 * @return true or false 1928 */ isValueLocked()1929 public boolean isValueLocked() { 1930 return valueIsLocked; 1931 } 1932 1933 /** 1934 * Can a user who makes a losing vote flag the locale+path? 1935 * I.e., is the locale+path locked and/or does it require HIGH_BAR votes? 1936 * 1937 * @return true or false 1938 */ canFlagOnLosing()1939 public boolean canFlagOnLosing() { 1940 return valueIsLocked || (requiredVotes == HIGH_BAR); 1941 } 1942 } 1943