• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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, &quot;J. Smith&quot;) },
42  *     { 555, new VoterInfo(Organization.google, Level.street, &quot;S. Jones&quot;) },
43  *     { 444, new VoterInfo(Organization.google, Level.vetter, &quot;S. Samuels&quot;) },
44  *     { 333, new VoterInfo(Organization.apple, Level.vetter, &quot;A. Mutton&quot;) },
45  *     { 222, new VoterInfo(Organization.adobe, Level.expert, &quot;A. Aldus&quot;) },
46  *     { 111, new VoterInfo(Organization.ibm, Level.street, &quot;J. Henry&quot;) }, }));
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 &lt;b&gt;must&lt;/b&gt; 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