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